2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import { withNoopInstall } from '../../utils'
import Picker from './src/picker.vue'
export const TnPicker = withNoopInstall(Picker)
export default TnPicker
export * from './src/picker'
export type { TnPickerInstance } from './src/instance'
@@ -0,0 +1,2 @@
export * from './picker-custom'
export * from './use-picker'
@@ -0,0 +1,72 @@
import { computed, toRef } from 'vue'
import { useComponentColor, useNamespace } from '../../../../hooks'
import type { CSSProperties } from 'vue'
import type { PickerProps } from '../picker'
export type PickerOperationBtnType = 'cancel' | 'confirm'
export type PickerOperationBtnClass = (type: PickerOperationBtnType) => string
export type PickerOperationBtnStyle = (
type: PickerOperationBtnType
) => CSSProperties
export const usePickerCustomStyle = (props: PickerProps) => {
const ns = useNamespace('picker')
// 解析颜色
const [cancelColorClass, cancelColorStyle] = useComponentColor(
toRef(props, 'cancelColor'),
'text'
)
const [confirmColorClass, confirmColorStyle] = useComponentColor(
toRef(props, 'confirmColor'),
'text'
)
// 遮罩的透明度
const overlayOpacity = computed(() => {
return props.mask ? 0.5 : 0
})
// 取消/确认按钮对应的类
const operationBtnClass = computed<PickerOperationBtnClass>(() => {
return (type: PickerOperationBtnType) => {
const cls: string[] = [
ns.e('operation-btn'),
ns.em('operation-btn', type),
]
if (type === 'cancel') {
if (cancelColorClass.value) cls.push(cancelColorClass.value)
} else if (type === 'confirm') {
if (confirmColorClass.value) cls.push(confirmColorClass.value)
}
return cls.join(' ')
}
})
// 取消/确认按钮对应的样式
const operationBtnStyle = computed<PickerOperationBtnStyle>(() => {
return (type: PickerOperationBtnType) => {
const style: CSSProperties = {}
if (type === 'cancel') {
if (!cancelColorClass.value)
style.color = cancelColorStyle.value || 'var(--tn-color-danger)'
} else if (type === 'confirm') {
if (!confirmColorClass.value)
style.color = confirmColorStyle.value || 'var(--tn-color-primary)'
}
return style
}
})
return {
ns,
overlayOpacity,
operationBtnClass,
operationBtnStyle,
}
}
@@ -0,0 +1,384 @@
import { getCurrentInstance, nextTick, ref, watch } from 'vue'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants'
import {
cloneDeep,
isArray,
isEmptyVariableInDefault,
isObject,
throwError,
} from '../../../../utils'
import type {
PickerDataType,
PickerMode,
PickerProps,
PickerValueType,
} from '../picker'
type PickerData = Array<Array<PickerDataItem>>
interface PickerDataItem {
label: string | number
value: string | number
originalData: any
children?: Array<PickerDataItem>
}
export const usePicker = (props: PickerProps) => {
const { emit } = getCurrentInstance()!
// 显示popup弹框
const openPopup = ref(false)
const showPicker = ref(true)
// #ifdef MP-ALIPAY
showPicker.value = false
// #endif
watch(
() => props.open,
(value) => {
openPopup.value = value
// #ifdef MP-ALIPAY
if (value) {
setTimeout(() => {
nextTick(() => {
showPicker.value = value
})
}, 350)
}
// #endif
}
)
const _closePopup = () => {
// #ifdef MP-ALIPAY
showPicker.value = false
// #endif
emit('update:open', false)
}
// 关闭popup弹框
const closePopupEvent = () => {
_closePopup()
_generatePickerViewData(props.modelValue)
emit('close')
}
// picker选择器类型
let pickerMode: PickerMode = 'signle'
// 生成指定格式的数据
const _generateData = (
data: PickerDataType
): Pick<PickerDataItem, 'label' | 'value' | 'originalData'> => {
if (isObject(data)) {
const originalData = cloneDeep(data)
if (
Object.prototype.hasOwnProperty.call(originalData, props.childrenKey)
) {
delete originalData[props.childrenKey]
}
return {
label: data[props.labelKey],
value: data[props.valueKey],
originalData,
}
} else {
return {
label: data as string | number,
value: data as string | number,
originalData: data,
}
}
}
// 更新/生成级联选择器的数据
const _generateOrUpdateCascadeData = (
data: any[],
generateIndex = 1,
defaultValue: Array<string | number> = []
) => {
// 判断生成的的级联数据是否已经有数据,如果有数据则更新,否则生成
if (pickerData.value.length < generateIndex) {
pickerData.value.push(
...Array.from(
{ length: generateIndex - pickerData.value.length },
() => []
)
)
}
pickerData.value[generateIndex - 1] = [
...data.map((item) => _generateData(item)),
]
// 判断从第几个子级开始生成级联数据
let childrenIndex = 0
if (defaultValue.length) {
childrenIndex = pickerData.value[generateIndex - 1].findIndex(
(item) => item.value === defaultValue[generateIndex - 1]
)
childrenIndex = ~childrenIndex ? childrenIndex : 0
}
if (
data[childrenIndex] &&
Object.prototype.hasOwnProperty.call(
data[childrenIndex],
props.childrenKey
)
) {
_generateOrUpdateCascadeData(
data[childrenIndex][props.childrenKey] as PickerDataItem[],
generateIndex + 1,
defaultValue
)
}
}
// picker选择器的数据
const pickerData = ref<PickerData>([])
// 当前选中picker-view的索引
const currentPickerIndex = ref<Array<number>>([])
// 初始化选中的默认Index
const initDefaultPickerIndex = () => {
let indexValue: number[] = []
// 如果没有设置默认值,则默认选中第一项
if (
props.modelValue === undefined ||
(!props.modelValue && ['multiple', 'cascade'].includes(pickerMode)) ||
(isArray(props.modelValue) && !props.modelValue.length)
) {
indexValue = Array.from({ length: pickerData.value.length }, () => 0)
} else {
if (isArray(props.modelValue)) {
indexValue = pickerData.value.map((item, index) => {
let pickerIndex = 0
if (!(props.modelValue as (string | number)[])[index]) pickerIndex = 0
else {
pickerIndex = item.findIndex((childItem) => {
return (
childItem.value ===
(props.modelValue as (string | number)[])[index]
)
})
}
return ~pickerIndex ? pickerIndex : 0
})
} else {
indexValue = pickerData.value.map((_, k: number) => {
const index = pickerData.value[k].findIndex(
(item) => item.value === props.modelValue
)
return index === -1 ? 0 : index
})
}
}
currentPickerIndex.value = indexValue
}
// 处理用户传递的数据
const splitUserPickerData = () => {
const { data } = props
if (!data) return
// 判断用户是否有传递数据,并且数据格式是否正确
if (!isArray(data)) {
throwError('TnPicker', 'picker选择器数据不正确,请传递数组格式的数据')
}
if (data.length === 0) return
// 根据用户传递的数据来判断是什么类型的选择器
if (isArray(data[0])) {
// 多选选择器
pickerMode = 'multiple'
pickerData.value = (data as PickerDataType[][]).reduce(
(prev: PickerData, cur: Array<PickerDataType>) => {
prev.push(cur.map((item) => _generateData(item)))
return prev
},
[]
)
} else if (
!isArray(data[0]) &&
isObject(data[0]) &&
Object.prototype.hasOwnProperty.call(data[0], props.childrenKey)
) {
// 级联选择器
pickerMode = 'cascade'
_generateOrUpdateCascadeData(
data as PickerDataItem[],
1,
props.modelValue as Array<string | number>
)
} else {
// 单列选择器
pickerMode = 'signle'
pickerData.value = [data.map((item) => _generateData(item))]
}
// console.log(JSON.stringify(pickerData.value))
nextTick(() => {
initDefaultPickerIndex()
})
}
watch(
() => props.data,
() => {
splitUserPickerData()
},
{
immediate: true,
}
)
// 获取当前选中的值
const _getCurrentPickerValue = (): PickerValueType => {
if (pickerMode === 'signle' && !isArray((props.data as any[])[0])) {
return pickerData.value[0][currentPickerIndex.value[0]].value
} else {
// currentPickerIndex.value.splice(pickerData.value.length)
const pickerIndex = cloneDeep(currentPickerIndex.value)
pickerIndex.splice(pickerData.value.length)
return pickerIndex.map((item, index) =>
isEmptyVariableInDefault(pickerData.value[index][item]?.value, 0)
)
}
}
// 根据用户选中的索引获取当前用户传入的数据
const _getCurrentPickerOriginData = (): any => {
if (pickerMode === 'signle' && !isArray((props.data as any[])[0])) {
return pickerData.value[0][currentPickerIndex.value[0]].originalData
} else {
// currentPickerIndex.value.splice(pickerData.value.length)
const pickerIndex = cloneDeep(currentPickerIndex.value)
pickerIndex.splice(pickerData.value.length)
return pickerIndex.map((item, index) =>
isEmptyVariableInDefault(
pickerData.value[index][item]?.originalData,
undefined
)
)
}
}
// 生成picker-view的数据
const _generatePickerViewData = (val: any) => {
// 如果是级联选择器,对应的级联数据也要更新
if (pickerMode === 'cascade') {
_generateOrUpdateCascadeData(
props.data as PickerDataItem[],
1,
val as Array<string | number>
)
}
nextTick(() => {
initDefaultPickerIndex()
})
}
// 标记是否内部更新
let isInnerUpdate = false
watch(
() => props.modelValue,
(val) => {
if (isInnerUpdate) {
isInnerUpdate = false
return
}
_generatePickerViewData(val)
},
{
deep: true,
}
)
// picker-view选择发生改变事件
let changeTimer: ReturnType<typeof setTimeout> | null = null
let continuousChangeStatus = false
const pickerViewChangeEvent = (e: any) => {
if (continuousChangeStatus) {
return
}
changeTimer = setTimeout(() => {
continuousChangeStatus = false
changeTimer && clearTimeout(changeTimer)
changeTimer = null
}, 300)
continuousChangeStatus = true
// 比较上一次的值,判断是那一列发生了改变
let changePickerColumnIndex = currentPickerIndex.value.findIndex(
(item, index) => item !== e.detail.value[index]
)
changePickerColumnIndex = ~changePickerColumnIndex
? changePickerColumnIndex
: 0
currentPickerIndex.value = e.detail.value
// 如果是级联选择器,对应的列有children的值,则需要更新后面的数据,并且重置后面选中的索引
if (pickerMode === 'cascade') {
let data: any[] = props.data as any[]
for (let i = 0; i < changePickerColumnIndex; i++) {
data = data[currentPickerIndex.value[i]][props.childrenKey]
}
const pickerIndex = currentPickerIndex.value[changePickerColumnIndex]
pickerData.value.splice(changePickerColumnIndex + 1)
if (
data[pickerIndex] &&
Object.prototype.hasOwnProperty.call(
data[pickerIndex],
props.childrenKey
)
) {
_generateOrUpdateCascadeData(
data[pickerIndex][props.childrenKey] as PickerDataItem[],
changePickerColumnIndex + 2
)
currentPickerIndex.value = pickerData.value.map((item, index) => {
return index <= changePickerColumnIndex
? currentPickerIndex.value[index]
: 0
})
}
}
isInnerUpdate = true
const value = _getCurrentPickerValue()
const originData = _getCurrentPickerOriginData()
emit(CHANGE_EVENT, value, changePickerColumnIndex, originData)
// emit(UPDATE_MODEL_EVENT, value)
}
// 重置指定位置的数据
const resetPickerIndexWithPosition = (start: number, end?: number) => {
currentPickerIndex.value = currentPickerIndex.value.map((item, index) => {
return index >= start && (!end || index <= end) ? 0 : item
})
}
// 点击确认按钮
const confirmEvent = () => {
const value = _getCurrentPickerValue()
const originData = _getCurrentPickerOriginData()
isInnerUpdate = true
emit(UPDATE_MODEL_EVENT, value)
nextTick(() => {
emit('confirm', value, originData)
})
_closePopup()
}
// 点击取消按钮
const cancelEvent = () => {
_generatePickerViewData(props.modelValue)
emit('cancel')
_closePopup()
}
return {
openPopup,
showPicker,
pickerData,
currentPickerIndex,
closePopupEvent,
pickerViewChangeEvent,
confirmEvent,
cancelEvent,
initDefaultPickerIndex,
resetPickerIndexWithPosition,
}
}
@@ -0,0 +1,3 @@
import type Picker from './picker.vue'
export type TnPickerInstance = InstanceType<typeof Picker>
+79
View File
@@ -0,0 +1,79 @@
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../constants'
import {
buildProps,
definePropType,
isArray,
isBoolean,
isNumber,
isString,
} from '../../../utils'
import { pickerBaseProps } from '../../base/common-props/picker'
import type { ExtractPropTypes } from 'vue'
import type { Arrayable } from '../../../utils'
export type PickerValueType = Arrayable<string | number>
export type PickerDataType = string | number | object
export type PickerData = Arrayable<Array<PickerDataType> | object>
export const PickerModes = ['signle', 'multiple', 'cascade'] as const
export const pickerProps = buildProps({
...pickerBaseProps,
/**
* @description picker绑定的值
*/
modelValue: {
type: definePropType<PickerValueType>([String, Number, Array]),
default: '',
},
/**
* @description 显示picker选项弹框
*/
open: Boolean,
/**
* @description picker选项的数据
*/
data: {
type: definePropType<PickerData>([Array]),
default: () => [],
},
/**
* @description picker选项的数据label属性名
*/
labelKey: {
type: String,
default: 'label',
},
/**
* @description picker选项的数据value属性名
*/
valueKey: {
type: String,
default: 'value',
},
/**
* @description picker选项的数据children属性名, 在级联选择器模式下生效
*/
childrenKey: {
type: String,
default: 'children',
},
})
export const pickerEmits = {
[UPDATE_MODEL_EVENT]: (value: PickerValueType) =>
isString(value) || isNumber(value) || isArray(value),
'update:open': (value: boolean) => isBoolean(value),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[CHANGE_EVENT]: (value: PickerValueType, index: number, item: any) => true,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
confirm: (value: PickerValueType, item: any) => true,
cancel: () => true,
close: () => true,
}
export type PickerProps = ExtractPropTypes<typeof pickerProps>
export type PickerEmits = typeof pickerEmits
export type PickerMode = (typeof PickerModes)[number]
+109
View File
@@ -0,0 +1,109 @@
<script lang="ts" setup>
import TnPopup from '../../popup/src/popup.vue'
import { pickerEmits, pickerProps } from './picker'
import { usePicker, usePickerCustomStyle } from './composables'
const props = defineProps(pickerProps)
defineEmits(pickerEmits)
const {
openPopup,
showPicker,
pickerData,
currentPickerIndex,
closePopupEvent,
pickerViewChangeEvent,
confirmEvent,
cancelEvent,
initDefaultPickerIndex,
resetPickerIndexWithPosition,
} = usePicker(props)
const { ns, overlayOpacity, operationBtnClass, operationBtnStyle } =
usePickerCustomStyle(props)
const resetPickerViewIndex = () => {
initDefaultPickerIndex()
}
defineExpose({
/**
* @description: 重置选择器的值
*/
resetPickerViewIndex,
/**
* @description: 重置指定位置选择器的值
*/
resetPickerIndexWithPosition,
})
</script>
<template>
<TnPopup
v-model="openPopup"
open-direction="bottom"
:overlay="true"
:overlay-opacity="overlayOpacity"
:radius="0"
:safe-area-inset-bottom="false"
:z-index="zIndex"
@close="closePopupEvent"
>
<view class="tn-u-safe-area" :class="[ns.b()]">
<!-- 操作按钮 -->
<view :class="[ns.e('operation'), ns.is('only-confirm', !showCancel)]">
<view
v-if="showCancel"
:class="[operationBtnClass('cancel')]"
:style="operationBtnStyle('cancel')"
@tap.stop="cancelEvent"
>
<slot name="cancel">
{{ props.cancelText }}
</slot>
</view>
<view
:class="[operationBtnClass('confirm')]"
:style="operationBtnStyle('confirm')"
@tap.stop="confirmEvent"
>
<slot name="confirm">
{{ props.confirmText }}
</slot>
</view>
</view>
<!-- 内容区域 -->
<view :class="[ns.e('content')]">
<picker-view
v-if="showPicker"
:class="[ns.e('picker-view')]"
:value="currentPickerIndex"
@change="pickerViewChangeEvent"
>
<picker-view-column
v-for="(item, index) in pickerData"
:key="index"
:class="[ns.e('picker-view-column')]"
>
<view
v-for="(dItem, dIndex) in item"
:key="dIndex"
:class="ns.e('content-item')"
>
<view
class="tn-text-ellipsis-1"
:class="ns.em('content-item', 'data')"
>
{{ dItem['label'] }}
</view>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</TnPopup>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/picker.scss';
</style>