init
This commit is contained in:
105
uni_modules/nutui-uni/components/pickercolumn/index.scss
Normal file
105
uni_modules/nutui-uni/components/pickercolumn/index.scss
Normal file
@@ -0,0 +1,105 @@
|
||||
.nut-theme-dark {
|
||||
.nut-picker {
|
||||
&-roller {
|
||||
color: $dark-color;
|
||||
|
||||
&-item {
|
||||
color: $dark-color;
|
||||
}
|
||||
|
||||
&-item-tile {
|
||||
color: $dark-color;
|
||||
}
|
||||
|
||||
&-item-tarotile {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-roller-mask {
|
||||
z-index: 1;
|
||||
background-image: linear-gradient(180deg, rgb(27 27 27 / 90%), rgb(27 27 27 / 40%)),
|
||||
linear-gradient(0deg, rgb(27 27 27 / 90%), rgb(27 27 27 / 40%));
|
||||
background-repeat: no-repeat;
|
||||
background-position: top, bottom;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-picker {
|
||||
&__list {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
&-roller {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: var(--line-height);
|
||||
color: $picker-item-text-color;
|
||||
transform: translateY(-50%);
|
||||
transform-style: preserve-3d;
|
||||
|
||||
&-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: $picker-item-height;
|
||||
overflow: hidden;
|
||||
font-size: $picker-item-text-font-size;
|
||||
line-height: $picker-item-height;
|
||||
color: $picker-item-text-color;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
backface-visibility: hidden;
|
||||
|
||||
&-hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-item-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: $picker-item-text-font-size;
|
||||
color: $picker-item-text-color;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-item-tarotile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-size: $picker-item-text-font-size;
|
||||
color: $picker-item-text-color;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-roller-mask {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(180deg, rgb(255 255 255 / 90%), rgb(255 255 255 / 40%)),
|
||||
linear-gradient(0deg, rgb(255 255 255 / 90%), rgb(255 255 255 / 40%));
|
||||
background-repeat: no-repeat;
|
||||
background-position: top, bottom;
|
||||
}
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/pickercolumn/index.ts
Normal file
2
uni_modules/nutui-uni/components/pickercolumn/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './pickercolumn'
|
||||
export * from './type'
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
import { CHANGE_EVENT } from '../_constants'
|
||||
import { makeArrayProp, makeNumericProp } from '../_utils'
|
||||
import type { PickerFieldNames, PickerOption } from './type'
|
||||
|
||||
export const pickercolumnProps = {
|
||||
/**
|
||||
* @description 当前选中项
|
||||
*/
|
||||
value: [String, Number],
|
||||
/**
|
||||
* @description 显示的数据
|
||||
*/
|
||||
column: makeArrayProp<PickerOption>([]),
|
||||
/**
|
||||
* @description 是否开启3D效果
|
||||
*/
|
||||
threeDimensional: Boolean,
|
||||
/**
|
||||
* @description 惯性滚动时长
|
||||
*/
|
||||
swipeDuration: makeNumericProp(1000),
|
||||
/**
|
||||
* @description 可见的选项个数
|
||||
*/
|
||||
visibleOptionNum: makeNumericProp(7),
|
||||
/**
|
||||
* @description 选项高度
|
||||
*/
|
||||
optionHeight: makeNumericProp(36),
|
||||
/**
|
||||
* @description 自定义 columns 中的字段
|
||||
*/
|
||||
fieldNames: {
|
||||
type: Object as PropType<Required<PickerFieldNames>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
/**
|
||||
* @description 特殊环境判断
|
||||
*/
|
||||
uni: Boolean,
|
||||
}
|
||||
|
||||
export type PickerColumnProps = ExtractPropTypes<typeof pickercolumnProps>
|
||||
|
||||
export const pickercolumnEmits = {
|
||||
[CHANGE_EVENT]: (val: PickerOption) => val instanceof Object || Array.isArray(val) || val === undefined,
|
||||
}
|
||||
|
||||
export type PickerColumnEmits = typeof pickercolumnEmits
|
||||
334
uni_modules/nutui-uni/components/pickercolumn/pickercolumn.vue
Normal file
334
uni_modules/nutui-uni/components/pickercolumn/pickercolumn.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { CHANGE_EVENT, PREFIX } from '../_constants'
|
||||
import { useTouch } from '../_hooks'
|
||||
import { clamp, preventDefault, pxCheck } from '../_utils'
|
||||
import { pickercolumnEmits, pickercolumnProps } from './pickercolumn'
|
||||
import type { PickerTouchParams } from './type'
|
||||
|
||||
const props = defineProps(pickercolumnProps)
|
||||
|
||||
const emit = defineEmits(pickercolumnEmits)
|
||||
|
||||
const DEFAULT_DURATION = 200
|
||||
|
||||
// 触发惯性滑动条件:
|
||||
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move
|
||||
// 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动
|
||||
const INERTIA_TIME = 300
|
||||
const INERTIA_DISTANCE = 15
|
||||
|
||||
const innerSwipeDuration = computed(() => {
|
||||
return Number(props.swipeDuration)
|
||||
})
|
||||
|
||||
const innerVisibleOptionNum = computed(() => {
|
||||
return Number(props.visibleOptionNum)
|
||||
})
|
||||
|
||||
const innerOptionHeight = computed(() => {
|
||||
return Number(props.optionHeight)
|
||||
})
|
||||
|
||||
interface State {
|
||||
touchParams: PickerTouchParams
|
||||
currIndex: number
|
||||
transformY: number
|
||||
scrollDistance: number
|
||||
rotation: number
|
||||
}
|
||||
|
||||
const state: State = reactive({
|
||||
touchParams: {
|
||||
startY: 0,
|
||||
endY: 0,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
lastY: 0,
|
||||
lastTime: 0,
|
||||
},
|
||||
currIndex: 1,
|
||||
transformY: 0,
|
||||
scrollDistance: 0,
|
||||
rotation: 20,
|
||||
})
|
||||
|
||||
const roller = ref<any | null>(null)
|
||||
|
||||
const moving = ref<boolean>(false)
|
||||
const touchDeg = ref<string>('0')
|
||||
const touchTime = ref<number>(0)
|
||||
|
||||
const rollerStyles = computed(() => {
|
||||
return {
|
||||
top: `calc(50% - ${innerOptionHeight.value / 2}px)`,
|
||||
transform: `rotate3d(1, 0, 0, ${touchDeg.value})`,
|
||||
transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
|
||||
}
|
||||
})
|
||||
|
||||
const tileStyles = computed(() => {
|
||||
return {
|
||||
top: `calc(50% - ${innerOptionHeight.value / 2}px)`,
|
||||
height: `${innerOptionHeight.value}px`,
|
||||
transform: `translate3d(0, ${state.scrollDistance}px, 0)`,
|
||||
transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
|
||||
}
|
||||
})
|
||||
|
||||
const maskStyles = computed(() => {
|
||||
return {
|
||||
backgroundSize: `100% ${((innerVisibleOptionNum.value - 1) * innerOptionHeight.value) / 2}px`,
|
||||
}
|
||||
})
|
||||
|
||||
function getRollerStyles(index: number) {
|
||||
return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`
|
||||
}
|
||||
|
||||
const touch = useTouch()
|
||||
|
||||
function onTouchStart(event: any) {
|
||||
touch.start(event)
|
||||
|
||||
if (moving.value && !props.uni) {
|
||||
const { transform } = window.getComputedStyle(roller.value?.$el)
|
||||
|
||||
if (props.threeDimensional) {
|
||||
const circle = Math.floor(Number.parseInt(touchDeg.value) / 360)
|
||||
const cos = Number(transform.split(', ')[5])
|
||||
const sin = Number(transform.split(', ')[6]) < 0 ? 180 : 0
|
||||
const endDeg = circle * 360 + (Math.acos(cos) / Math.PI) * 180 + sin
|
||||
|
||||
state.scrollDistance = -Math.abs((endDeg / state.rotation - 1) * innerOptionHeight.value)
|
||||
}
|
||||
else {
|
||||
state.scrollDistance = Number(transform.slice(7, transform.length - 1).split(', ')[5])
|
||||
}
|
||||
}
|
||||
|
||||
preventDefault(event, true)
|
||||
|
||||
state.touchParams.startY = touch.deltaY.value
|
||||
state.touchParams.startTime = Date.now()
|
||||
state.transformY = state.scrollDistance
|
||||
}
|
||||
|
||||
function onTouchMove(event: any) {
|
||||
touch.move(event)
|
||||
|
||||
if (touch.isVertical()) {
|
||||
moving.value = true
|
||||
preventDefault(event, true)
|
||||
}
|
||||
|
||||
state.touchParams.lastY = touch.deltaY.value
|
||||
|
||||
setMove(state.touchParams.lastY - state.touchParams.startY)
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
state.touchParams.lastY = touch.deltaY.value
|
||||
state.touchParams.lastTime = Date.now()
|
||||
|
||||
const move = state.touchParams.lastY - state.touchParams.startY
|
||||
const moveTime = state.touchParams.lastTime - state.touchParams.startTime
|
||||
|
||||
if (moveTime <= INERTIA_TIME && Math.abs(move) > INERTIA_DISTANCE) {
|
||||
// 惯性滚动
|
||||
const distance = momentum(move, moveTime)
|
||||
setMove(distance, 'end', innerSwipeDuration.value)
|
||||
return
|
||||
}
|
||||
|
||||
setMove(move, 'end')
|
||||
|
||||
setTimeout(() => {
|
||||
touch.reset()
|
||||
|
||||
moving.value = false
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算惯性滚动距离
|
||||
*/
|
||||
function momentum(distance: number, duration: number) {
|
||||
// 惯性滚动的速度
|
||||
const speed = Math.abs(distance / duration)
|
||||
// 惯性滚动的距离
|
||||
distance = (speed / 0.003) * (distance < 0 ? -1 : 1)
|
||||
return distance
|
||||
}
|
||||
|
||||
function isHidden(index: number) {
|
||||
return index >= state.currIndex + 8 || index <= state.currIndex - 8
|
||||
}
|
||||
|
||||
function setTransform(translateY = 0, type: string | null, time = DEFAULT_DURATION, deg: string) {
|
||||
if (type === 'end')
|
||||
touchTime.value = time
|
||||
else
|
||||
touchTime.value = 0
|
||||
|
||||
touchDeg.value = deg
|
||||
|
||||
state.scrollDistance = translateY
|
||||
}
|
||||
|
||||
function setMove(move: number, type?: string, time?: number) {
|
||||
let updateMove = move + state.transformY
|
||||
|
||||
if (type === 'end') {
|
||||
// 限定滚动距离
|
||||
if (updateMove > 0)
|
||||
updateMove = 0
|
||||
|
||||
const maxMove = (props.column.length - 1) * innerOptionHeight.value
|
||||
if (updateMove < -maxMove)
|
||||
updateMove = -maxMove
|
||||
|
||||
// 设置滚动距离为 optionHeight 的倍数值
|
||||
const endMove = Math.round(updateMove / innerOptionHeight.value) * innerOptionHeight.value
|
||||
const deg = `${(Math.abs(Math.round(endMove / innerOptionHeight.value)) + 1) * state.rotation}deg`
|
||||
|
||||
setTransform(endMove, type, time, deg)
|
||||
|
||||
state.currIndex = Math.abs(Math.round(endMove / innerOptionHeight.value)) + 1
|
||||
}
|
||||
else {
|
||||
const currentDeg = (-updateMove / innerOptionHeight.value + 1) * state.rotation
|
||||
|
||||
// picker 滚动的最大角度
|
||||
const maxDeg = (props.column.length + 1) * state.rotation
|
||||
const minDeg = 0
|
||||
|
||||
const deg = clamp(currentDeg, minDeg, maxDeg)
|
||||
|
||||
if (minDeg < deg && deg < maxDeg) {
|
||||
setTransform(updateMove, null, undefined, `${deg}deg`)
|
||||
|
||||
state.currIndex = Math.abs(Math.round(updateMove / innerOptionHeight.value)) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setChooseValue() {
|
||||
emit(CHANGE_EVENT, props.column[state.currIndex - 1])
|
||||
}
|
||||
|
||||
function modifyStatus(type: boolean) {
|
||||
const index = props.column.findIndex(columnItem => columnItem[props.fieldNames.value] === props.value)
|
||||
|
||||
state.currIndex = index === -1 ? 1 : index + 1
|
||||
|
||||
const move = index === -1 ? 0 : index * innerOptionHeight.value
|
||||
type && setChooseValue()
|
||||
setMove(-move)
|
||||
}
|
||||
|
||||
// 惯性滚动结束
|
||||
function stopMomentum() {
|
||||
moving.value = false
|
||||
touchTime.value = 0
|
||||
|
||||
setChooseValue()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.column,
|
||||
() => {
|
||||
if (props.column && props.column.length > 0) {
|
||||
state.transformY = 0
|
||||
|
||||
modifyStatus(false)
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
state.transformY = 0
|
||||
|
||||
modifyStatus(false)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
modifyStatus(true)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
stopMomentum,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-picker-column`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="nut-picker__list"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<view
|
||||
ref="roller"
|
||||
class="nut-picker-roller"
|
||||
:style="[props.threeDimensional ? rollerStyles : tileStyles]"
|
||||
@transitionend="stopMomentum"
|
||||
>
|
||||
<template
|
||||
v-for="(item, index) in props.column"
|
||||
:key="item[props.fieldNames.value] ? item[props.fieldNames.value] : index"
|
||||
>
|
||||
<!-- 3D 效果 -->
|
||||
<view
|
||||
v-if="item && item[props.fieldNames.text] && props.threeDimensional"
|
||||
class="nut-picker-roller-item"
|
||||
:class="{
|
||||
'nut-picker-roller-item-hidden': isHidden(index + 1),
|
||||
[item[props.fieldNames.className]]: item[props.fieldNames.className],
|
||||
}"
|
||||
:style="getRollerStyles(index + 1)"
|
||||
>
|
||||
{{ item[props.fieldNames.text] }}
|
||||
</view>
|
||||
|
||||
<!-- 平铺 -->
|
||||
<view
|
||||
v-if="item && item[props.fieldNames.text] && !props.threeDimensional"
|
||||
class="nut-picker-roller-item-tile"
|
||||
:class="{ [item[props.fieldNames.className]]: item[props.fieldNames.className] }"
|
||||
:style="{ height: pxCheck(innerOptionHeight), lineHeight: pxCheck(innerOptionHeight) }"
|
||||
>
|
||||
{{ item[props.fieldNames.text] }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="nut-picker-roller-mask" :style="maskStyles" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./index";
|
||||
</style>
|
||||
24
uni_modules/nutui-uni/components/pickercolumn/type.ts
Normal file
24
uni_modules/nutui-uni/components/pickercolumn/type.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface PickerOption {
|
||||
text?: string | number
|
||||
value?: string | number
|
||||
disabled?: string
|
||||
children?: PickerOption[]
|
||||
className?: string | number
|
||||
[key: PropertyKey]: any
|
||||
}
|
||||
|
||||
export interface PickerTouchParams {
|
||||
startY: number
|
||||
endY: number
|
||||
startTime: number
|
||||
endTime: number
|
||||
lastY: number
|
||||
lastTime: number
|
||||
}
|
||||
|
||||
export interface PickerFieldNames {
|
||||
text?: string
|
||||
value?: string
|
||||
children?: string
|
||||
className?: string
|
||||
}
|
||||
Reference in New Issue
Block a user