init
This commit is contained in:
18
uni_modules/nutui-uni/components/_constants/event.ts
Normal file
18
uni_modules/nutui-uni/components/_constants/event.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const UPDATE_MODEL_EVENT = 'update:modelValue'
|
||||
export const UPDATE_VISIBLE_EVENT = 'update:visible'
|
||||
export const CHANGE_EVENT = 'change'
|
||||
export const INPUT_EVENT = 'input'
|
||||
export const CLICK_EVENT = 'click'
|
||||
export const OPEN_EVENT = 'open'
|
||||
export const CLOSE_EVENT = 'close'
|
||||
export const OPENED_EVENT = 'opened'
|
||||
export const CLOSED_EVENT = 'closed'
|
||||
export const FOCUS_EVENT = 'focus'
|
||||
export const BLUR_EVENT = 'blur'
|
||||
export const CONFIRM_EVENT = 'confirm'
|
||||
export const SEARCH_EVENT = 'search'
|
||||
export const CLEAR_EVENT = 'clear'
|
||||
export const CANCEL_EVENT = 'cancel'
|
||||
export const CHOOSE_EVENT = 'choose'
|
||||
export const SELECT_EVENT = 'select'
|
||||
export const SELECTED_EVENT = 'selected'
|
||||
3
uni_modules/nutui-uni/components/_constants/index.ts
Normal file
3
uni_modules/nutui-uni/components/_constants/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './event'
|
||||
export * from './prefix'
|
||||
export * from './types'
|
||||
1
uni_modules/nutui-uni/components/_constants/prefix.ts
Normal file
1
uni_modules/nutui-uni/components/_constants/prefix.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PREFIX = 'nut'
|
||||
11
uni_modules/nutui-uni/components/_constants/types.ts
Normal file
11
uni_modules/nutui-uni/components/_constants/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { NutAnimationName } from '../transition'
|
||||
|
||||
export type Position = 'center' | 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
export const animationName: Record<Position, NutAnimationName> = {
|
||||
center: 'fade',
|
||||
top: 'slide-down',
|
||||
bottom: 'slide-up',
|
||||
left: 'slide-left',
|
||||
right: 'slide-right',
|
||||
}
|
||||
11
uni_modules/nutui-uni/components/_hooks/index.ts
Normal file
11
uni_modules/nutui-uni/components/_hooks/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './useExpose'
|
||||
export * from './useGlobalZIndex'
|
||||
export * from './useInject'
|
||||
export * from './useLockScroll'
|
||||
export * from './useProvide'
|
||||
export * from './useRect'
|
||||
export * from './useRelation'
|
||||
export * from './useRouter'
|
||||
export * from './useSelectorQuery'
|
||||
export * from './useStyle'
|
||||
export * from './useTouch'
|
||||
8
uni_modules/nutui-uni/components/_hooks/useExpose.ts
Normal file
8
uni_modules/nutui-uni/components/_hooks/useExpose.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
export function useExpose(apis: Record<string, any>) {
|
||||
const instance = getCurrentInstance()
|
||||
if (instance)
|
||||
Object.assign(instance.proxy as ComponentPublicInstance, apis)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
let globalZIndex = 2000
|
||||
|
||||
export function useGlobalZIndex() {
|
||||
return ++globalZIndex
|
||||
}
|
||||
|
||||
export function setGlobalZIndex(value: number) {
|
||||
globalZIndex = value
|
||||
}
|
||||
32
uni_modules/nutui-uni/components/_hooks/useInject.ts
Normal file
32
uni_modules/nutui-uni/components/_hooks/useInject.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { computed, getCurrentInstance, inject, onUnmounted, ref } from 'vue'
|
||||
import type { ComponentInternalInstance, InjectionKey } from 'vue'
|
||||
|
||||
type ParentProvide<T> = T & {
|
||||
add: (child: ComponentInternalInstance) => void
|
||||
remove: (child: ComponentInternalInstance) => void
|
||||
internalChildren: ComponentInternalInstance[]
|
||||
}
|
||||
|
||||
export function useInject<T>(key: InjectionKey<ParentProvide<T>>) {
|
||||
const parent = inject(key, null)
|
||||
|
||||
if (parent) {
|
||||
const instance = getCurrentInstance()!
|
||||
const { add, remove, internalChildren } = parent
|
||||
|
||||
add(instance)
|
||||
onUnmounted(() => remove(instance))
|
||||
|
||||
const index = computed(() => internalChildren.indexOf(instance))
|
||||
|
||||
return {
|
||||
parent,
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parent: null,
|
||||
index: ref(-1),
|
||||
}
|
||||
}
|
||||
31
uni_modules/nutui-uni/components/_hooks/useLockScroll.ts
Normal file
31
uni_modules/nutui-uni/components/_hooks/useLockScroll.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
let count = 0
|
||||
|
||||
const CLSNAME = 'nut-overflow-hidden'
|
||||
|
||||
export function useLockScroll(isLock: () => boolean) {
|
||||
const lock = () => {
|
||||
if (isLock()) {
|
||||
try {
|
||||
!count && document.body.classList.add(CLSNAME)
|
||||
count++
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unlock = () => {
|
||||
if (isLock() && count) {
|
||||
try {
|
||||
count--
|
||||
!count && document.body.classList.remove(CLSNAME)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [lock, unlock]
|
||||
}
|
||||
93
uni_modules/nutui-uni/components/_hooks/useProvide.ts
Normal file
93
uni_modules/nutui-uni/components/_hooks/useProvide.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { getCurrentInstance, markRaw, provide, shallowReactive } from 'vue'
|
||||
import type {
|
||||
ComponentInternalInstance,
|
||||
ConcreteComponent,
|
||||
InjectionKey,
|
||||
VNode,
|
||||
VNodeNormalizedChildren,
|
||||
} from 'vue'
|
||||
|
||||
// TODO: uniapp 不支持 vue 直接导出的 isVNode
|
||||
export function isVNode(value: any): value is VNode {
|
||||
return value ? value.__v_isVNode === true : false
|
||||
}
|
||||
|
||||
export function flattenVNodes(shouldTraverseChildren: VNodeNormalizedChildren, childName?: string) {
|
||||
const result: VNode[] = []
|
||||
|
||||
const traverse = (children: VNodeNormalizedChildren) => {
|
||||
if (!Array.isArray(children))
|
||||
return
|
||||
children.forEach((child) => {
|
||||
if (!isVNode(child))
|
||||
return
|
||||
|
||||
if (childName) {
|
||||
if (child.type && (child.type as ConcreteComponent).name === childName) {
|
||||
result.push(child)
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.push(child)
|
||||
}
|
||||
|
||||
if (child.component?.subTree)
|
||||
traverse(child.component.subTree.children)
|
||||
|
||||
if (child.children)
|
||||
traverse(child.children)
|
||||
})
|
||||
}
|
||||
|
||||
traverse(shouldTraverseChildren)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function sortChildren(
|
||||
parent: ComponentInternalInstance,
|
||||
internalChildren: ComponentInternalInstance[],
|
||||
childName?: string,
|
||||
) {
|
||||
const vnodes = flattenVNodes(parent && parent.subTree && parent.subTree.children, childName)
|
||||
internalChildren.sort((a, b) => {
|
||||
return vnodes.indexOf(a.vnode) - vnodes.indexOf(b.vnode)
|
||||
})
|
||||
}
|
||||
|
||||
// 如果指定组件名称,则只查找此组件并且查到后结束。也就是不关心此组件下的内容,在大部分场景下节省查找消耗。
|
||||
export function useProvide<ProvideValue>(key: InjectionKey<ProvideValue>, childName?: string) {
|
||||
const internalChildren: ComponentInternalInstance[] = shallowReactive([])
|
||||
const publicChildren = shallowReactive<any[]>([])
|
||||
const parent = getCurrentInstance()!
|
||||
|
||||
const add = (child: ComponentInternalInstance) => {
|
||||
if (!child.proxy)
|
||||
return
|
||||
internalChildren.push(markRaw(child))
|
||||
publicChildren.push(markRaw(child.proxy))
|
||||
sortChildren(parent, internalChildren, childName)
|
||||
}
|
||||
|
||||
const remove = (child: ComponentInternalInstance) => {
|
||||
if (child.proxy) {
|
||||
internalChildren.splice(internalChildren.indexOf(markRaw(child)), 1)
|
||||
publicChildren.splice(publicChildren.indexOf(markRaw(child.proxy)), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (value?: ProvideValue) => {
|
||||
provide(key, {
|
||||
add,
|
||||
remove,
|
||||
internalChildren,
|
||||
...value,
|
||||
} as any)
|
||||
|
||||
return {
|
||||
internalChildren,
|
||||
children: publicChildren,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
uni_modules/nutui-uni/components/_hooks/useRect.ts
Normal file
7
uni_modules/nutui-uni/components/_hooks/useRect.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ComponentInternalInstance } from 'vue'
|
||||
import { useSelectorQuery } from './useSelectorQuery'
|
||||
|
||||
export function useRect(id: string, instance?: ComponentInternalInstance): Promise<UniApp.NodeInfo> {
|
||||
const { getSelectorNodeInfo } = useSelectorQuery(instance)
|
||||
return getSelectorNodeInfo(`#${id}`)
|
||||
}
|
||||
8
uni_modules/nutui-uni/components/_hooks/useRelation.ts
Normal file
8
uni_modules/nutui-uni/components/_hooks/useRelation.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
export function useExtend<T>(apis: T) {
|
||||
const instance = getCurrentInstance()
|
||||
if (instance)
|
||||
Object.assign(instance.proxy as ComponentPublicInstance, apis)
|
||||
}
|
||||
28
uni_modules/nutui-uni/components/_hooks/useRouter.ts
Normal file
28
uni_modules/nutui-uni/components/_hooks/useRouter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type NavigateToOptions = string | UniApp.NavigateToOptions
|
||||
export type RedirectToOptions = string | UniApp.RedirectToOptions
|
||||
|
||||
export type RouterOptions = UniApp.NavigateToOptions | UniApp.RedirectToOptions
|
||||
|
||||
export function useRouter() {
|
||||
const push = (options: NavigateToOptions) => {
|
||||
if (typeof options === 'string') {
|
||||
uni.navigateTo({ url: options })
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo(options)
|
||||
}
|
||||
const replace = (options: RedirectToOptions) => {
|
||||
if (typeof options === 'string') {
|
||||
uni.redirectTo({ url: options })
|
||||
return
|
||||
}
|
||||
|
||||
uni.redirectTo(options)
|
||||
}
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
}
|
||||
}
|
||||
67
uni_modules/nutui-uni/components/_hooks/useSelectorQuery.ts
Normal file
67
uni_modules/nutui-uni/components/_hooks/useSelectorQuery.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import type { ComponentInternalInstance } from 'vue'
|
||||
|
||||
export function useSelectorQuery(instance?: ComponentInternalInstance | null) {
|
||||
let query: UniApp.SelectorQuery | null = null
|
||||
|
||||
if (!instance)
|
||||
instance = getCurrentInstance()
|
||||
|
||||
if (!instance)
|
||||
console.warn('useSelectorQuery', 'useSelectorQuery必须在setup函数中使用')
|
||||
|
||||
// #ifndef MP-ALIPAY
|
||||
query = uni.createSelectorQuery().in(instance)
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
query = uni.createSelectorQuery().in(null)
|
||||
// #endif
|
||||
|
||||
const getSelectorNodeInfo = (selector: string): Promise<UniApp.NodeInfo> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (query) {
|
||||
query
|
||||
.select(selector)
|
||||
.boundingClientRect((res) => {
|
||||
const selectRes: UniApp.NodeInfo = res as UniApp.NodeInfo
|
||||
if (selectRes)
|
||||
resolve(selectRes)
|
||||
else
|
||||
reject(new Error(`未找到对应节点: ${selector}`))
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
else {
|
||||
reject(new Error('未找到对应的SelectorQuery实例'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSelectorNodeInfos = (
|
||||
selector: string,
|
||||
): Promise<UniApp.NodeInfo[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (query) {
|
||||
query
|
||||
.selectAll(selector)
|
||||
.boundingClientRect((res) => {
|
||||
const selectRes: UniApp.NodeInfo[] = res as UniApp.NodeInfo[]
|
||||
if (selectRes && selectRes.length > 0)
|
||||
resolve(selectRes)
|
||||
else
|
||||
reject(new Error(`未找到对应节点: ${selector}`))
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
else {
|
||||
reject(new Error('未找到对应的SelectorQuery实例'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
getSelectorNodeInfo,
|
||||
getSelectorNodeInfos,
|
||||
}
|
||||
}
|
||||
29
uni_modules/nutui-uni/components/_hooks/useStyle.ts
Normal file
29
uni_modules/nutui-uni/components/_hooks/useStyle.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { computed, normalizeClass, normalizeStyle } from 'vue'
|
||||
import { stringifyStyle } from '../_utils'
|
||||
|
||||
export function useStyleContext(props: any, componentName: string) {
|
||||
const mainClass = computed(() => {
|
||||
const cls = normalizeClass([props.customClass, { [componentName]: true }])
|
||||
|
||||
return cls
|
||||
})
|
||||
|
||||
const mainStyle = computed(() => {
|
||||
return stringifyStyle(normalizeStyle(props.customStyle))
|
||||
})
|
||||
|
||||
const getMainClass = (cls: unknown) => {
|
||||
return normalizeClass([props.customClass, { [componentName]: true }, cls])
|
||||
}
|
||||
|
||||
const getMainStyle = (style: unknown) => {
|
||||
return stringifyStyle(normalizeStyle([props.customStyle, style]))
|
||||
}
|
||||
|
||||
return {
|
||||
mainClass,
|
||||
mainStyle,
|
||||
getMainClass,
|
||||
getMainStyle,
|
||||
}
|
||||
}
|
||||
74
uni_modules/nutui-uni/components/_hooks/useTouch.ts
Normal file
74
uni_modules/nutui-uni/components/_hooks/useTouch.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const MIN_DISTANCE = 10
|
||||
|
||||
type Direction = '' | 'vertical' | 'horizontal'
|
||||
|
||||
function getDirection(x: number, y: number) {
|
||||
if (x > y && x > MIN_DISTANCE)
|
||||
return 'horizontal'
|
||||
|
||||
if (y > x && y > MIN_DISTANCE)
|
||||
return 'vertical'
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function useTouch() {
|
||||
const startX = ref(0)
|
||||
const startY = ref(0)
|
||||
const moveX = ref(0)
|
||||
const moveY = ref(0)
|
||||
const deltaX = ref(0)
|
||||
const deltaY = ref(0)
|
||||
const offsetX = ref(0)
|
||||
const offsetY = ref(0)
|
||||
const direction = ref<Direction>('')
|
||||
|
||||
const isVertical = () => direction.value === 'vertical'
|
||||
const isHorizontal = () => direction.value === 'horizontal'
|
||||
|
||||
const reset = () => {
|
||||
deltaX.value = 0
|
||||
deltaY.value = 0
|
||||
offsetX.value = 0
|
||||
offsetY.value = 0
|
||||
direction.value = ''
|
||||
}
|
||||
|
||||
const start = ((event: TouchEvent) => {
|
||||
reset()
|
||||
startX.value = event.touches[0].clientX
|
||||
startY.value = event.touches[0].clientY
|
||||
}) as EventListener
|
||||
|
||||
const move = ((event: TouchEvent) => {
|
||||
const touch = event.touches[0]
|
||||
deltaX.value = touch.clientX - startX.value
|
||||
deltaY.value = touch.clientY - startY.value
|
||||
moveX.value = touch.clientX
|
||||
moveY.value = touch.clientY
|
||||
offsetX.value = Math.abs(deltaX.value)
|
||||
offsetY.value = Math.abs(deltaY.value)
|
||||
|
||||
if (!direction.value)
|
||||
direction.value = getDirection(offsetX.value, offsetY.value)
|
||||
}) as EventListener
|
||||
|
||||
return {
|
||||
move,
|
||||
start,
|
||||
reset,
|
||||
startX,
|
||||
startY,
|
||||
moveX,
|
||||
moveY,
|
||||
deltaX,
|
||||
deltaY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
direction,
|
||||
isVertical,
|
||||
isHorizontal,
|
||||
}
|
||||
}
|
||||
277
uni_modules/nutui-uni/components/_utils/common.ts
Normal file
277
uni_modules/nutui-uni/components/_utils/common.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { isArray, isDef, isObject } from './is'
|
||||
|
||||
// 变量类型判断
|
||||
export function TypeOfFun(value: any) {
|
||||
if (value === null)
|
||||
return 'null'
|
||||
|
||||
const type = typeof value
|
||||
if (type === 'undefined' || type === 'string')
|
||||
return type
|
||||
|
||||
const typeString = toString.call(value)
|
||||
switch (typeString) {
|
||||
case '[object Array]':
|
||||
return 'array'
|
||||
case '[object Date]':
|
||||
return 'date'
|
||||
case '[object Boolean]':
|
||||
return 'boolean'
|
||||
case '[object Number]':
|
||||
return 'number'
|
||||
case '[object Function]':
|
||||
return 'function'
|
||||
case '[object RegExp]':
|
||||
return 'regexp'
|
||||
case '[object Object]':
|
||||
if (undefined !== value.nodeType) {
|
||||
if (value.nodeType === 3)
|
||||
return /\S/.test(value.nodeValue) ? 'textnode' : 'whitespace'
|
||||
else
|
||||
return 'element'
|
||||
}
|
||||
else {
|
||||
return 'object'
|
||||
}
|
||||
default:
|
||||
return 'unknow'
|
||||
}
|
||||
}
|
||||
//
|
||||
export const objectToString = Object.prototype.toString
|
||||
export const toTypeString = (value: unknown): string => objectToString.call(value)
|
||||
|
||||
export function toRawType(value: unknown): string {
|
||||
// extract "RawType" from strings like "[object RawType]"
|
||||
return toTypeString(value).slice(8, -1)
|
||||
}
|
||||
|
||||
export const win = window
|
||||
|
||||
export const docu = document
|
||||
|
||||
export const body = docu.body
|
||||
|
||||
export function getPropByPath(obj: any, keyPath: string) {
|
||||
try {
|
||||
return keyPath.split('.').reduce((prev, curr) => prev[curr], obj)
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function floatData(format: any, dataOp: any, mapOps: any) {
|
||||
const mergeFormat = Object.assign({}, format)
|
||||
const mergeMapOps = Object.assign({}, mapOps)
|
||||
|
||||
if (Object.keys(dataOp).length > 0) {
|
||||
Object.keys(mergeFormat).forEach((keys) => {
|
||||
if (Object.prototype.hasOwnProperty.call(mergeMapOps, keys)) {
|
||||
const tof = TypeOfFun(mergeMapOps[keys])
|
||||
if (tof === 'function')
|
||||
mergeFormat[keys] = mergeMapOps[keys](dataOp)
|
||||
|
||||
if (tof === 'string')
|
||||
mergeFormat[keys] = dataOp[mergeMapOps[keys]]
|
||||
}
|
||||
else {
|
||||
if (dataOp[keys])
|
||||
mergeFormat[keys] = dataOp[keys]
|
||||
}
|
||||
})
|
||||
return mergeFormat
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
export function myFixed(num: any, digit = 2) {
|
||||
if (Object.is(Number.parseFloat(num), Number.NaN))
|
||||
return console.warn(`传入的值:${num}不是一个数字`)
|
||||
|
||||
num = Number.parseFloat(num)
|
||||
return (Math.round((num + Number.EPSILON) * 10 ** digit) / 10 ** digit).toFixed(digit)
|
||||
}
|
||||
|
||||
export function preventDefault(event: Event, isStopPropagation?: boolean) {
|
||||
if (typeof event.cancelable !== 'boolean' || event.cancelable)
|
||||
event.preventDefault()
|
||||
|
||||
if (isStopPropagation)
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
function cacheStringFunction<T extends (str: string) => string>(fn: T): T {
|
||||
const cache: Record<string, string> = Object.create(null)
|
||||
return ((str: string) => {
|
||||
const hit = cache[str]
|
||||
|
||||
return hit || (cache[str] = fn(str))
|
||||
}) as T
|
||||
}
|
||||
|
||||
const hyphenateRE = /\B([A-Z])/g
|
||||
export const hyphenate = cacheStringFunction((str: string) =>
|
||||
str.replace(hyphenateRE, '-$1').toLowerCase(),
|
||||
)
|
||||
|
||||
export function padZero(num: number | string, length = 2): string {
|
||||
num += ''
|
||||
while ((num as string).length < length)
|
||||
num = `0${num}`
|
||||
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
export const clamp = (num: number, min: number, max: number): number => Math.min(Math.max(num, min), max)
|
||||
|
||||
export function getScrollTopRoot(): number {
|
||||
return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
}
|
||||
|
||||
type ObjectIndex = Record<string, unknown>
|
||||
|
||||
const { hasOwnProperty } = Object.prototype
|
||||
|
||||
function assignKey(to: ObjectIndex, from: ObjectIndex, key: string) {
|
||||
const val = from[key]
|
||||
|
||||
if (!isDef(val))
|
||||
return
|
||||
|
||||
if (!hasOwnProperty.call(to, key) || !isObject(val))
|
||||
to[key] = val
|
||||
|
||||
else
|
||||
// eslint-disable-next-line unicorn/new-for-builtins
|
||||
to[key] = deepAssign(Object(to[key]), val)
|
||||
}
|
||||
|
||||
export function deepAssign(to: ObjectIndex, from: ObjectIndex): ObjectIndex {
|
||||
Object.keys(from).forEach((key) => {
|
||||
assignKey(to, from, key)
|
||||
})
|
||||
|
||||
return to
|
||||
}
|
||||
|
||||
export function omit(obj: Record<string, unknown>, keys: string[]) {
|
||||
if (Object.prototype.toString.call(obj) === '[object Object]')
|
||||
return obj
|
||||
|
||||
return Object.keys(obj).reduce((prev, key) => {
|
||||
if (!keys.includes(key))
|
||||
prev[key] = obj[key]
|
||||
|
||||
return prev
|
||||
}, {} as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export interface Deferred<T> extends Promise<T> {
|
||||
resolve: (value?: T) => void
|
||||
reject: (value?: any) => void
|
||||
}
|
||||
|
||||
export function createDeferred<T>(): Deferred<T> {
|
||||
let resolve: Deferred<T>['resolve'] = noop
|
||||
let reject: Deferred<T>['reject'] = noop
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve
|
||||
reject = _reject
|
||||
}) as unknown as Deferred<T>
|
||||
|
||||
promise.resolve = resolve
|
||||
promise.reject = reject
|
||||
return promise
|
||||
}
|
||||
|
||||
export function toArray<T>(value?: T | T[]): T[] {
|
||||
if (!value)
|
||||
return []
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
export function noop() { }
|
||||
|
||||
export function getRandomId() {
|
||||
return Math.random().toString(36).slice(-8)
|
||||
}
|
||||
|
||||
export function isLooseEqual(a: any, b: any): boolean {
|
||||
if (a === b)
|
||||
return true
|
||||
|
||||
const isObjectA = isObject(a)
|
||||
const isObjectB = isObject(b)
|
||||
|
||||
if (isObjectA && isObjectB)
|
||||
return JSON.stringify(a) === JSON.stringify(b)
|
||||
else if (!isObjectA && !isObjectB)
|
||||
return String(a) === String(b)
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
export function isEqualArray(a: any, b: any): boolean {
|
||||
if (a === b)
|
||||
return true
|
||||
|
||||
if (!isArray(a) || !isArray(b))
|
||||
return false
|
||||
|
||||
if (a.length !== b.length)
|
||||
return false
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!isLooseEqual(a[i], b[i]))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function isEqualValue(a: any, b: any): boolean {
|
||||
if (a === b)
|
||||
return true
|
||||
|
||||
if (isArray(a) && isArray(b))
|
||||
return isEqualArray(a, b)
|
||||
|
||||
return isLooseEqual(a, b)
|
||||
}
|
||||
|
||||
export function cloneDeep<T = any>(obj: T, cache = new WeakMap()): T {
|
||||
if (obj === null || typeof obj !== 'object')
|
||||
return obj
|
||||
if (cache.has(obj))
|
||||
return cache.get(obj)
|
||||
let clone
|
||||
if (obj instanceof Date) {
|
||||
clone = new Date(obj.getTime())
|
||||
}
|
||||
else if (obj instanceof RegExp) {
|
||||
clone = new RegExp(obj)
|
||||
}
|
||||
else if (obj instanceof Map) {
|
||||
clone = new Map(Array.from(obj, ([key, value]) => [key, cloneDeep(value, cache)]))
|
||||
}
|
||||
else if (obj instanceof Set) {
|
||||
clone = new Set(Array.from(obj, value => cloneDeep(value, cache)))
|
||||
}
|
||||
else if (Array.isArray(obj)) {
|
||||
clone = obj.map(value => cloneDeep(value, cache))
|
||||
}
|
||||
else if (Object.prototype.toString.call(obj) === '[object Object]') {
|
||||
clone = Object.create(Object.getPrototypeOf(obj))
|
||||
cache.set(obj, clone)
|
||||
for (const [key, value] of Object.entries(obj))
|
||||
clone[key] = cloneDeep(value, cache)
|
||||
}
|
||||
else {
|
||||
clone = Object.assign({}, obj)
|
||||
}
|
||||
cache.set(obj, clone)
|
||||
return clone
|
||||
}
|
||||
167
uni_modules/nutui-uni/components/_utils/date.ts
Normal file
167
uni_modules/nutui-uni/components/_utils/date.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 时间戳转换 或 获取当前时间的时间戳
|
||||
*/
|
||||
export function getTimeStamp(timeStr?: string | number) {
|
||||
if (!timeStr)
|
||||
return Date.now()
|
||||
let t = timeStr
|
||||
t = (t as number > 0) ? +t : t.toString().replace(/-/g, '/')
|
||||
return new Date(t).getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为闫年
|
||||
* @return {Boolse} true|false
|
||||
*/
|
||||
export function isLeapYear(y: number): boolean {
|
||||
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回星期数
|
||||
* @return {string}
|
||||
*/
|
||||
export function getWhatDay(year: number, month: number, day: number): string {
|
||||
const date = new Date(`${year}/${month}/${day}`)
|
||||
const index = date.getDay()
|
||||
const dayNames = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
return dayNames[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回星期数
|
||||
* @return {number}
|
||||
*/
|
||||
export function getMonthPreDay(year: number, month: number): number {
|
||||
const date = new Date(`${year}/${month}/01`)
|
||||
let day = date.getDay()
|
||||
if (day === 0)
|
||||
day = 7
|
||||
|
||||
return day
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回月份天数
|
||||
* @return {number}
|
||||
*/
|
||||
export function getMonthDays(year: string, month: string): number {
|
||||
if (month.startsWith('0'))
|
||||
month = month.split('')[1]
|
||||
|
||||
return ([0, 31, isLeapYear(Number(year)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as number[])[
|
||||
month as any
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 补齐数字位数
|
||||
* @return {string}
|
||||
*/
|
||||
export function getNumTwoBit(n: number): string {
|
||||
n = Number(n)
|
||||
return (n > 9 ? '' : '0') + n
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期对象转成字符串
|
||||
* @return {string}
|
||||
*/
|
||||
export function date2Str(date: Date, split?: string): string {
|
||||
split = split || '-'
|
||||
const y = date.getFullYear()
|
||||
const m = getNumTwoBit(date.getMonth() + 1)
|
||||
const d = getNumTwoBit(date.getDate())
|
||||
return [y, m, d].join(split)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回日期格式字符串
|
||||
* @param i 0返回今天的日期、1返回明天的日期,2返回后天得日期,依次类推
|
||||
* @return {string} '2014-12-31'
|
||||
*/
|
||||
export function getDay(i: number): string {
|
||||
i = i || 0
|
||||
let date = new Date()
|
||||
const diff = i * (1000 * 60 * 60 * 24)
|
||||
date = new Date(date.getTime() + diff)
|
||||
return date2Str(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间比较
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function compareDate(date1: string, date2: string): boolean {
|
||||
const startTime = new Date(date1.replace('-', '/').replace('-', '/'))
|
||||
const endTime = new Date(date2.replace('-', '/').replace('-', '/'))
|
||||
if (startTime >= endTime)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间是否相等
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isEqual(date1: string, date2: string): boolean {
|
||||
const startTime = new Date(date1).getTime()
|
||||
const endTime = new Date(date2).getTime()
|
||||
if (startTime === endTime)
|
||||
return true
|
||||
|
||||
return false
|
||||
}
|
||||
export function getMonthWeek(year: string, month: string, date: string, firstDayOfWeek = 0): number {
|
||||
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
|
||||
let w = dateNow.getDay() // 星期数
|
||||
const d = dateNow.getDate()
|
||||
let remainder = 6 - w
|
||||
if (firstDayOfWeek !== 0) {
|
||||
w = w === 0 ? 7 : w
|
||||
remainder = 7 - w
|
||||
}
|
||||
return Math.ceil((d + remainder) / 7)
|
||||
}
|
||||
export function getYearWeek(year: string, month: string, date: string): number {
|
||||
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
|
||||
const dateFirst = new Date(Number(year), 0, 1)
|
||||
const dataNumber = Math.round((dateNow.valueOf() - dateFirst.valueOf()) / 86400000)
|
||||
return Math.ceil((dataNumber + (dateFirst.getDay() + 1 - 1)) / 7)
|
||||
}
|
||||
export function getWeekDate(year: string, month: string, date: string, firstDayOfWeek = 0): string[] {
|
||||
const dateNow = new Date(Number(year), Number.parseInt(month) - 1, Number(date))
|
||||
const nowTime = dateNow.getTime()
|
||||
let day = dateNow.getDay()
|
||||
if (firstDayOfWeek === 0) {
|
||||
const oneDayTime = 24 * 60 * 60 * 1000
|
||||
// 显示周日
|
||||
const SundayTime = nowTime - day * oneDayTime // 本周的周日
|
||||
// 显示周六
|
||||
const SaturdayTime = nowTime + (6 - day) * oneDayTime // 本周的周六
|
||||
|
||||
const sunday = date2Str(new Date(SundayTime))
|
||||
const saturday = date2Str(new Date(SaturdayTime))
|
||||
return [sunday, saturday]
|
||||
}
|
||||
else {
|
||||
day = day === 0 ? 7 : day
|
||||
const oneDayTime = 24 * 60 * 60 * 1000
|
||||
// 显示周一
|
||||
const MondayTime = nowTime - (day - 1) * oneDayTime // 本周的周一
|
||||
// 显示周日
|
||||
const SundayTime = nowTime + (7 - day) * oneDayTime // 本周的周日
|
||||
|
||||
const monday = date2Str(new Date(MondayTime))
|
||||
const sunday = date2Str(new Date(SundayTime))
|
||||
return [monday, sunday]
|
||||
}
|
||||
}
|
||||
export function formatResultDate(date: string) {
|
||||
const days = [...date.split('-')]
|
||||
days[2] = getNumTwoBit(Number(days[2]))
|
||||
days[3] = `${days[0]}-${days[1]}-${days[2]}`
|
||||
days[4] = getWhatDay(+days[0], +days[1], +days[2])
|
||||
return days
|
||||
}
|
||||
100
uni_modules/nutui-uni/components/_utils/env.ts
Normal file
100
uni_modules/nutui-uni/components/_utils/env.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/** 枚举EPlatform */
|
||||
enum EPlatform {
|
||||
/** App */
|
||||
AppPlus = 'APP-PLUS',
|
||||
/** App nvue */
|
||||
AppPlusNvue = 'APP-PLUS-NVUE',
|
||||
/** H5 */
|
||||
H5 = 'H5',
|
||||
/** 微信小程序 */
|
||||
MpWeixin = 'MP-WEIXIN',
|
||||
/** 支付宝小程序 */
|
||||
MpAlipay = 'MP-ALIPAY',
|
||||
/** 百度小程序 */
|
||||
MpBaidu = 'MP-BAIDU',
|
||||
/** 字节跳动小程序 */
|
||||
MpToutiao = 'MP-TOUTIAO',
|
||||
/** QQ小程序 */
|
||||
MpQq = 'MP-QQ',
|
||||
/** 360小程序 */
|
||||
Mp360 = 'MP-360',
|
||||
/** 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/QQ小程序/360小程序 */
|
||||
Mp = 'MP',
|
||||
/** 快应用通用(包含联盟、华为) */
|
||||
QuickappWebview = 'quickapp-webview',
|
||||
/** 快应用联盟 */
|
||||
QuickappWebviewUnion = 'quickapp-webview-union',
|
||||
/** 快应用华为 */
|
||||
QuickappWebviewHuawei = 'quickapp-webview-huawei',
|
||||
}
|
||||
|
||||
/** 使用条件编译获取平台信息 */
|
||||
function ifDefPlatform(): EPlatform {
|
||||
let platform: EPlatform
|
||||
// #ifdef APP-PLUS
|
||||
platform = EPlatform.AppPlus
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
platform = EPlatform.AppPlusNvue
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
platform = EPlatform.H5
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
platform = EPlatform.MpWeixin
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
platform = EPlatform.MpAlipay
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU
|
||||
platform = EPlatform.MpBaidu
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
platform = EPlatform.MpToutiao
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
platform = EPlatform.MpQq
|
||||
// #endif
|
||||
// #ifdef MP-360
|
||||
platform = EPlatform.Mp360
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
platform = EPlatform.Mp
|
||||
// #endif
|
||||
// #ifdef quickapp-webview
|
||||
platform = EPlatform.QuickappWebview
|
||||
// #endif
|
||||
// #ifdef quickapp-webview-union
|
||||
platform = EPlatform.QuickappWebviewUnion
|
||||
// #endif
|
||||
// #ifdef quickapp-webview-huawei
|
||||
platform = EPlatform.QuickappWebviewHuawei
|
||||
// #endif
|
||||
return platform
|
||||
}
|
||||
|
||||
/** 平台类型 */
|
||||
export const platform: EPlatform = ifDefPlatform()
|
||||
|
||||
/** H5 */
|
||||
export const isH5 = platform === EPlatform.H5
|
||||
/** 微信小程序 */
|
||||
export const isMpWeixin = platform === EPlatform.MpWeixin
|
||||
/** 支付宝小程序 */
|
||||
export const isMpAlipay = platform === EPlatform.MpAlipay
|
||||
/** 百度小程序 */
|
||||
export const isMpBaidu = platform === EPlatform.MpBaidu
|
||||
/** 字节跳动小程序 */
|
||||
export const isMpToutiao = platform === EPlatform.MpToutiao
|
||||
/** QQ小程序 */
|
||||
export const isMpQq = platform === EPlatform.MpQq
|
||||
/** 360小程序 */
|
||||
export const isMp360 = platform === EPlatform.Mp360
|
||||
/** 微信小程序/支付宝小程序/百度小程序/字节跳动小程序/QQ小程序/360小程序 */
|
||||
export const isMp = platform === EPlatform.Mp
|
||||
/** 快应用通用(包含联盟、华为) */
|
||||
export const isQuickappWebview = platform === EPlatform.QuickappWebview
|
||||
/** 快应用联盟 */
|
||||
export const isQuickappWebviewUnion = platform === EPlatform.QuickappWebviewUnion
|
||||
/** 快应用华为 */
|
||||
export const isQuickappWebviewHuawei = platform === EPlatform.QuickappWebviewHuawei
|
||||
9
uni_modules/nutui-uni/components/_utils/index.ts
Normal file
9
uni_modules/nutui-uni/components/_utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './common'
|
||||
export * from './date'
|
||||
export * from './env'
|
||||
export * from './interceptor'
|
||||
export * from './is'
|
||||
export * from './props'
|
||||
export * from './pxCheck'
|
||||
export * from './raf'
|
||||
export * from './style'
|
||||
37
uni_modules/nutui-uni/components/_utils/interceptor.ts
Normal file
37
uni_modules/nutui-uni/components/_utils/interceptor.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { isPromise } from './is'
|
||||
|
||||
export type Interceptor = (...args: any[]) => Promise<boolean> | boolean | undefined | void
|
||||
|
||||
export function funInterceptor(interceptor: Interceptor | undefined, {
|
||||
args = [],
|
||||
done,
|
||||
canceled,
|
||||
}: {
|
||||
args?: unknown[]
|
||||
done: (val?: any) => void
|
||||
canceled?: () => void
|
||||
}) {
|
||||
if (interceptor) {
|
||||
const returnVal = interceptor(null, ...args)
|
||||
|
||||
if (isPromise(returnVal)) {
|
||||
returnVal
|
||||
.then((value) => {
|
||||
if (value)
|
||||
done(value)
|
||||
else if (canceled)
|
||||
canceled()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
else if (returnVal) {
|
||||
done()
|
||||
}
|
||||
else if (canceled) {
|
||||
canceled()
|
||||
}
|
||||
}
|
||||
else {
|
||||
done()
|
||||
}
|
||||
}
|
||||
96
uni_modules/nutui-uni/components/_utils/is.ts
Normal file
96
uni_modules/nutui-uni/components/_utils/is.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
const toString = Object.prototype.toString
|
||||
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`
|
||||
}
|
||||
|
||||
export function isDef<T = unknown>(val?: T): val is T {
|
||||
return typeof val !== 'undefined'
|
||||
}
|
||||
|
||||
export function isUnDef<T = unknown>(val?: T): val is T {
|
||||
return !isDef(val)
|
||||
}
|
||||
|
||||
export function isObject(val: any): val is Record<any, any> {
|
||||
return val !== null && is(val, 'Object')
|
||||
}
|
||||
|
||||
export function isEmpty<T = unknown>(val: T): val is T {
|
||||
if (isArray(val) || isString(val))
|
||||
return val.length === 0
|
||||
|
||||
if (val instanceof Map || val instanceof Set)
|
||||
return val.size === 0
|
||||
|
||||
if (isObject(val))
|
||||
return Object.keys(val).length === 0
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function isDate(val: unknown): val is Date {
|
||||
return is(val, 'Date')
|
||||
}
|
||||
|
||||
export function isNull(val: unknown): val is null {
|
||||
return val === null
|
||||
}
|
||||
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) && isNull(val)
|
||||
}
|
||||
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) || isNull(val)
|
||||
}
|
||||
|
||||
export function isNumber(val: unknown): val is number {
|
||||
return is(val, 'Number')
|
||||
}
|
||||
|
||||
export function isPromise<T = any>(val: unknown): val is Promise<T> {
|
||||
return (
|
||||
is(val, 'Promise')
|
||||
|| ((isObject(val) || isFunction(val))
|
||||
&& isFunction((val as any).then)
|
||||
&& isFunction((val as any).catch))
|
||||
)
|
||||
}
|
||||
|
||||
export function isString(val: unknown): val is string {
|
||||
return is(val, 'String')
|
||||
}
|
||||
|
||||
export function isFunction(val: unknown): val is () => void {
|
||||
return typeof val === 'function'
|
||||
}
|
||||
|
||||
export function isBoolean(val: unknown): val is boolean {
|
||||
return is(val, 'Boolean')
|
||||
}
|
||||
|
||||
export function isRegExp(val: unknown): val is RegExp {
|
||||
return is(val, 'RegExp')
|
||||
}
|
||||
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val)
|
||||
}
|
||||
|
||||
export function isWindow(val: any): val is Window {
|
||||
return typeof window !== 'undefined' && is(val, 'Window')
|
||||
}
|
||||
|
||||
export function isElement(val: unknown): val is Element {
|
||||
return isObject(val) && !!val.tagName
|
||||
}
|
||||
|
||||
export function isMap(val: unknown): val is Map<any, any> {
|
||||
return is(val, 'Map')
|
||||
}
|
||||
|
||||
export function isUrl(path: string): boolean {
|
||||
const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/
|
||||
return reg.test(path)
|
||||
}
|
||||
83
uni_modules/nutui-uni/components/_utils/props.ts
Normal file
83
uni_modules/nutui-uni/components/_utils/props.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* prop type helpers
|
||||
* help us to write less code and reduce bundle size
|
||||
* copy from https://github.com/youzan/vant/blob/main/packages/vant/src/utils/props.ts
|
||||
*/
|
||||
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
|
||||
|
||||
export const unknownProp = null as unknown as PropType<unknown>
|
||||
|
||||
export const numericProp = [Number, String]
|
||||
|
||||
export const truthProp = {
|
||||
type: Boolean,
|
||||
default: true as const,
|
||||
}
|
||||
|
||||
export const nullableBooleanProp = {
|
||||
type: Boolean as PropType<boolean | undefined>,
|
||||
default: undefined,
|
||||
}
|
||||
|
||||
export function makeRequiredProp<T>(type: T) {
|
||||
return {
|
||||
type,
|
||||
required: true as const,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeArrayProp<T>(defaultVal: T[] = []) {
|
||||
return {
|
||||
type: Array as PropType<T[]>,
|
||||
default: () => defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeObjectProp<T>(defaultVal: T) {
|
||||
return {
|
||||
type: Object as PropType<T>,
|
||||
default: () => defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNumberProp<T>(defaultVal: T) {
|
||||
return {
|
||||
type: Number,
|
||||
default: defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeNumericProp<T>(defaultVal: T) {
|
||||
return {
|
||||
type: numericProp,
|
||||
default: defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeStringProp<T>(defaultVal: T) {
|
||||
return {
|
||||
type: String as unknown as PropType<T>,
|
||||
default: defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
export type ClassType = string | object | Array<ClassType>
|
||||
|
||||
export const commonProps = {
|
||||
/**
|
||||
* @description 自定义类名
|
||||
*/
|
||||
customClass: {
|
||||
type: [String, Object, Array] as PropType<ClassType>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description 自定义样式
|
||||
*/
|
||||
customStyle: {
|
||||
type: [String, Object, Array] as PropType<StyleValue>,
|
||||
default: '',
|
||||
},
|
||||
}
|
||||
|
||||
export type CommonProps = ExtractPropTypes<typeof commonProps>
|
||||
3
uni_modules/nutui-uni/components/_utils/pxCheck.ts
Normal file
3
uni_modules/nutui-uni/components/_utils/pxCheck.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function pxCheck(value: string | number): string {
|
||||
return Number.isNaN(Number(value)) ? String(value) : `${value}px`
|
||||
}
|
||||
30
uni_modules/nutui-uni/components/_utils/raf.ts
Normal file
30
uni_modules/nutui-uni/components/_utils/raf.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
const _window = window as any
|
||||
|
||||
export const inBrowser = typeof window !== 'undefined'
|
||||
|
||||
function requestAniFrame() {
|
||||
if (typeof _window !== 'undefined') {
|
||||
return (
|
||||
_window.requestAnimationFrame
|
||||
|| _window.webkitRequestAnimationFrame
|
||||
|| function (callback: () => void) {
|
||||
_window.setTimeout(callback, 1000 / 60)
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
return function (callback: () => void) {
|
||||
setTimeout(callback, 1000 / 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelRaf(id: number) {
|
||||
if (inBrowser)
|
||||
cancelAnimationFrame(id)
|
||||
|
||||
else
|
||||
clearTimeout(id)
|
||||
}
|
||||
|
||||
export default requestAniFrame()
|
||||
167
uni_modules/nutui-uni/components/_utils/style.ts
Normal file
167
uni_modules/nutui-uni/components/_utils/style.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { hyphenate } from './common'
|
||||
import { isArray, isEmpty, isNumber, isObject, isString } from './is'
|
||||
import type { CommonProps } from './props'
|
||||
|
||||
export type NormalizedStyle = Record<string, string | number>
|
||||
|
||||
const listDelimiterRE = /;(?![^(]*\))/g
|
||||
const propertyDelimiterRE = /:([\s\S]+)/
|
||||
const styleCommentRE = /\/\*.*?\*\//g
|
||||
|
||||
export function parseStringStyle(cssText: string): NormalizedStyle {
|
||||
const ret: NormalizedStyle = {}
|
||||
cssText
|
||||
.replace(styleCommentRE, '')
|
||||
.split(listDelimiterRE)
|
||||
.forEach((item) => {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiterRE)
|
||||
tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
export function stringifyStyle(styles: NormalizedStyle | string | undefined): string {
|
||||
let ret = ''
|
||||
if (!styles || isString(styles))
|
||||
return ret
|
||||
|
||||
for (const key in styles) {
|
||||
const value = styles[key]
|
||||
const normalizedKey = key.startsWith('--') ? key : hyphenate(key)
|
||||
if (isString(value) || typeof value === 'number') {
|
||||
// only render valid values
|
||||
ret += `${normalizedKey}:${value};`
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
export function getPx(value: string | number, unit = false) {
|
||||
if (isNumber(value))
|
||||
return unit ? `${value}px` : Number(value)
|
||||
|
||||
return unit ? `${Number.parseInt(value)}px` : Number.parseInt(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 样式转换
|
||||
* 对象转字符串,或者字符串转对象
|
||||
* @param {object | string} customStyle 需要转换的目标
|
||||
* @param {string} target 转换的目的,object-转为对象,string-转为字符串
|
||||
*/
|
||||
export function addStyle(customStyle: string | object, target = 'object') {
|
||||
// 字符串转字符串,对象转对象情形,直接返回
|
||||
if (
|
||||
isEmpty(customStyle)
|
||||
|| (typeof customStyle === 'object' && target === 'object')
|
||||
|| (target === 'string' && typeof customStyle === 'string')
|
||||
) {
|
||||
return customStyle
|
||||
}
|
||||
|
||||
// 字符串转对象
|
||||
if (target === 'object') {
|
||||
// 去除字符串样式中的两端空格(中间的空格不能去掉,比如padding: 20px 0如果去掉了就错了),空格是无用的
|
||||
customStyle = trim(customStyle)
|
||||
// 根据";"将字符串转为数组形式
|
||||
const styleArray = customStyle.split(';')
|
||||
const style: any = {}
|
||||
// 历遍数组,拼接成对象
|
||||
for (let i = 0; i < styleArray.length; i++) {
|
||||
// 'font-size:20px;color:red;',如此最后字符串有";"的话,会导致styleArray最后一个元素为空字符串,这里需要过滤
|
||||
if (styleArray[i]) {
|
||||
const item = styleArray[i].split(':')
|
||||
style[trim(item[0])] = trim(item[1])
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
// 这里为对象转字符串形式
|
||||
let string = ''
|
||||
for (const i in customStyle as any) {
|
||||
// 驼峰转为中划线的形式,否则css内联样式,无法识别驼峰样式属性名
|
||||
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
string += `${key}:${customStyle[i]};`
|
||||
}
|
||||
// 去除两端空格
|
||||
return trim(string)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 去除空格
|
||||
* @param str 需要去除空格的字符串
|
||||
* @param pos both(左右)|left|right|all 默认both
|
||||
*/
|
||||
export function trim(str: string, pos = 'both') {
|
||||
str = String(str)
|
||||
if (pos === 'both')
|
||||
return str.replace(/^\s+|\s+$/g, '')
|
||||
|
||||
if (pos === 'left')
|
||||
return str.replace(/^\s*/, '')
|
||||
|
||||
if (pos === 'right')
|
||||
return str.replace(/(\s*$)/g, '')
|
||||
|
||||
if (pos === 'all')
|
||||
return str.replace(/\s+/g, '')
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
export function normalizeStyle(value: unknown): NormalizedStyle | string | undefined {
|
||||
if (isArray(value)) {
|
||||
const res: NormalizedStyle = {}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const item = value[i]
|
||||
const normalized = isString(item)
|
||||
? parseStringStyle(item)
|
||||
: (normalizeStyle(item) as NormalizedStyle)
|
||||
if (normalized) {
|
||||
for (const key in normalized) {
|
||||
if (!isEmpty(normalized[key]))
|
||||
res[key] = normalized[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
if (isString(value))
|
||||
return value
|
||||
|
||||
if (isObject(value))
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizeClass(value: unknown): string {
|
||||
let res = ''
|
||||
if (isString(value)) {
|
||||
res = value
|
||||
}
|
||||
else if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const normalized = normalizeClass(value[i])
|
||||
if (normalized)
|
||||
res += `${normalized} `
|
||||
}
|
||||
}
|
||||
else if (isObject(value)) {
|
||||
for (const name in value) {
|
||||
if (value[name])
|
||||
res += `${name} `
|
||||
}
|
||||
}
|
||||
return res.trim()
|
||||
}
|
||||
|
||||
export function getMainClass(props: CommonProps, componentName: string, cls?: object) {
|
||||
return normalizeClass([props.customClass, { [componentName]: true }, cls])
|
||||
}
|
||||
|
||||
export function getMainStyle(props: CommonProps, style?: CSSProperties) {
|
||||
return stringifyStyle(normalizeStyle([props.customStyle, style]))
|
||||
}
|
||||
82
uni_modules/nutui-uni/components/actionsheet/actionsheet.ts
Normal file
82
uni_modules/nutui-uni/components/actionsheet/actionsheet.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
|
||||
import { CANCEL_EVENT, CHOOSE_EVENT, CLOSE_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { commonProps, isBoolean, isNumber, makeArrayProp, makeStringProp, truthProp } from '../_utils'
|
||||
import { popupProps } from '../popup/popup'
|
||||
|
||||
export interface ActionSheetOption {
|
||||
disable?: boolean
|
||||
loading?: boolean
|
||||
color?: string
|
||||
name: string
|
||||
subname?: string
|
||||
}
|
||||
|
||||
export const actionsheetProps = {
|
||||
...popupProps,
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 是否显示圆角
|
||||
*/
|
||||
round: truthProp,
|
||||
/**
|
||||
* @description 是否开启 iPhone 系列全面屏底部安全区适配,仅当 `position` 为 `bottom` 时有效
|
||||
*/
|
||||
safeAreaInsetBottom: truthProp,
|
||||
/**
|
||||
* @description 遮罩显示时的背景是否锁定
|
||||
*/
|
||||
lockScroll: truthProp,
|
||||
/**
|
||||
* @description 自定义 popup 弹框样式
|
||||
*/
|
||||
popStyle: {
|
||||
type: Object as PropType<CSSProperties>,
|
||||
},
|
||||
/**
|
||||
* @description 取消文案
|
||||
*/
|
||||
cancelTxt: makeStringProp(''),
|
||||
/**
|
||||
* @description 设置列表项标题展示使用参数
|
||||
*/
|
||||
optionTag: makeStringProp<keyof ActionSheetOption>('name'),
|
||||
/**
|
||||
* @description 设置列表项二级标题展示使用参数
|
||||
*/
|
||||
optionSubTag: makeStringProp<keyof ActionSheetOption>('subname'),
|
||||
/**
|
||||
* @description 设置选中项的值,和 'option-tag' 的值对应
|
||||
*/
|
||||
chooseTagValue: makeStringProp(''),
|
||||
/**
|
||||
* @description 设置列表项标题
|
||||
*/
|
||||
title: makeStringProp(''),
|
||||
/**
|
||||
* @description 选中项颜色,当 choose-tag-value == option-tag 的值 生效
|
||||
*/
|
||||
customColor: makeStringProp('#ee0a24'),
|
||||
/**
|
||||
* @description 设置列表项副标题/描述
|
||||
*/
|
||||
description: makeStringProp(''),
|
||||
/**
|
||||
* @description 列表项
|
||||
*/
|
||||
menuItems: makeArrayProp<ActionSheetOption>([]),
|
||||
/**
|
||||
* @description 遮罩层是否可关闭
|
||||
*/
|
||||
closeAbled: truthProp,
|
||||
}
|
||||
|
||||
export type ActionsheetProps = ExtractPropTypes<typeof actionsheetProps>
|
||||
|
||||
export const actionsheetEmits = {
|
||||
[CLOSE_EVENT]: () => true,
|
||||
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
|
||||
[CANCEL_EVENT]: () => true,
|
||||
[CHOOSE_EVENT]: (item: ActionSheetOption, index: number) => item instanceof Object && isNumber(index),
|
||||
}
|
||||
|
||||
export type ActionsheetEmits = typeof actionsheetEmits
|
||||
120
uni_modules/nutui-uni/components/actionsheet/actionsheet.vue
Normal file
120
uni_modules/nutui-uni/components/actionsheet/actionsheet.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, useSlots } from 'vue'
|
||||
import { CANCEL_EVENT, CHOOSE_EVENT, CLOSE_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import type { ActionSheetOption } from './actionsheet'
|
||||
import { actionsheetEmits, actionsheetProps } from './actionsheet'
|
||||
|
||||
const props = defineProps(actionsheetProps)
|
||||
|
||||
const emit = defineEmits(actionsheetEmits)
|
||||
|
||||
const slotDefault = !!useSlots().default
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
function isHighlight(item: ActionSheetOption) {
|
||||
return props.chooseTagValue && props.chooseTagValue === item[props.optionTag] ? props.customColor : ''
|
||||
}
|
||||
|
||||
function cancelActionSheet() {
|
||||
emit(CANCEL_EVENT)
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
|
||||
function chooseItem(item: ActionSheetOption, index: number) {
|
||||
if (!item.disable && !item.loading) {
|
||||
emit(CHOOSE_EVENT, item, index)
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (props.closeAbled) {
|
||||
emit(CLOSE_EVENT)
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-action-sheet`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NutPopup
|
||||
:pop-class="props.popClass"
|
||||
:custom-style="props.popStyle"
|
||||
:visible="props.visible"
|
||||
position="bottom"
|
||||
:overlay="props.overlay"
|
||||
:round="props.round"
|
||||
:safe-area-inset-bottom="props.safeAreaInsetBottom"
|
||||
:z-index="props.zIndex"
|
||||
:duration="props.duration"
|
||||
:overlay-class="props.overlayClass"
|
||||
:overlay-style="props.overlayStyle"
|
||||
:lock-scroll="props.lockScroll"
|
||||
:close-on-click-overlay="props.closeAbled"
|
||||
@click-overlay="close"
|
||||
>
|
||||
<view :class="classes" :style="props.customStyle">
|
||||
<view v-if="props.title" class="nut-action-sheet__title">
|
||||
{{ props.title }}
|
||||
</view>
|
||||
|
||||
<slot />
|
||||
|
||||
<view v-if="!slotDefault">
|
||||
<view v-if="props.description" class="nut-action-sheet__item nut-action-sheet__desc">
|
||||
{{ props.description }}
|
||||
</view>
|
||||
|
||||
<view v-if="props.menuItems.length" class="nut-action-sheet__menu">
|
||||
<view
|
||||
v-for="(item, index) of props.menuItems"
|
||||
:key="index"
|
||||
class="nut-action-sheet__item"
|
||||
:class="{
|
||||
'nut-action-sheet__item--disabled': item.disable,
|
||||
'nut-action-sheet__item--loading': item.loading,
|
||||
}"
|
||||
:style="{ color: isHighlight(item) || item.color }"
|
||||
@click="chooseItem(item, index)"
|
||||
>
|
||||
<NutIcon v-if="item.loading" name="loading" />
|
||||
|
||||
<view v-else>
|
||||
{{ item[props.optionTag] }}
|
||||
</view>
|
||||
|
||||
<view class="nut-action-sheet__subdesc">
|
||||
{{ item[props.optionSubTag] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="props.cancelTxt" class="nut-action-sheet__cancel" @click="cancelActionSheet">
|
||||
{{ props.cancelTxt }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</NutPopup>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
82
uni_modules/nutui-uni/components/actionsheet/index.scss
Normal file
82
uni_modules/nutui-uni/components/actionsheet/index.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
@import "../popup/index";
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-action-sheet {
|
||||
.nut-action-sheet__cancel {
|
||||
border-top: 1px solid $dark-background2;
|
||||
}
|
||||
|
||||
.nut-action-sheet__title {
|
||||
border-bottom: 1px solid $dark-background2;
|
||||
}
|
||||
|
||||
.nut-action-sheet__cancel,
|
||||
.nut-action-sheet__item,
|
||||
.nut-action-sheet__title {
|
||||
color: $dark-color;
|
||||
background: $dark-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-action-sheet {
|
||||
display: block;
|
||||
|
||||
.nut-action-sheet__title {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
font-size: $font-size-base;
|
||||
color: $title-color;
|
||||
text-align: center;
|
||||
background-color: $white;
|
||||
border-bottom: 1px solid $actionsheet-light-color;
|
||||
}
|
||||
|
||||
.nut-action-sheet__menu {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nut-action-sheet__cancel,
|
||||
.nut-action-sheet__item {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
font-size: $actionsheet-item-font-size;
|
||||
line-height: $actionsheet-item-line-height;
|
||||
color: $actionsheet-item-font-color;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border-bottom: $actionsheet-item-border-bottom;
|
||||
}
|
||||
|
||||
.nut-action-sheet__desc {
|
||||
font-size: $actionsheet-item-font-size;
|
||||
color: #999;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nut-action-sheet__subdesc {
|
||||
display: block;
|
||||
font-size: $actionsheet-item-subdesc-font-size;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.nut-action-sheet__item--disabled {
|
||||
color: #e1e1e1 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nut-action-sheet__item--loading {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nut-action-sheet__cancel {
|
||||
margin-top: 5px;
|
||||
border-top: $actionsheet-item-cancel-border-top;
|
||||
}
|
||||
}
|
||||
|
||||
1
uni_modules/nutui-uni/components/actionsheet/index.ts
Normal file
1
uni_modules/nutui-uni/components/actionsheet/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './actionsheet'
|
||||
104
uni_modules/nutui-uni/components/address/address.ts
Normal file
104
uni_modules/nutui-uni/components/address/address.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CHANGE_EVENT, CLOSE_EVENT, SELECTED_EVENT, UPDATE_MODEL_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { commonProps, isBoolean, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
import { popupProps } from '../popup'
|
||||
import type { AddressExistRegionData, AddressRegionData, AddressType } from './type'
|
||||
|
||||
export const addressProps = {
|
||||
...popupProps,
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 设置默认选中值
|
||||
*/
|
||||
modelValue: makeArrayProp<any>([]),
|
||||
|
||||
/**
|
||||
* @description 是否打开地址选择
|
||||
*/
|
||||
visible: Boolean,
|
||||
|
||||
/**
|
||||
* @description 地址选择类型:'exist' | 'custom' | 'custom2'
|
||||
*/
|
||||
type: makeStringProp<AddressType>('custom'),
|
||||
|
||||
/**
|
||||
* @description 自定义地址选择标题
|
||||
*/
|
||||
customAddressTitle: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 省份数据
|
||||
*/
|
||||
province: makeArrayProp<AddressRegionData>([]),
|
||||
|
||||
/**
|
||||
* @description 城市数据
|
||||
*/
|
||||
city: makeArrayProp<AddressRegionData>([]),
|
||||
|
||||
/**
|
||||
* @description 县区数据
|
||||
*/
|
||||
country: makeArrayProp<AddressRegionData>([]),
|
||||
|
||||
/**
|
||||
* @description 乡镇数据
|
||||
*/
|
||||
town: makeArrayProp<AddressRegionData>([]),
|
||||
|
||||
/**
|
||||
* @description 是否显示 '选择其他地区' 按钮。仅在类型为 'exist' 时生效
|
||||
*/
|
||||
isShowCustomAddress: truthProp,
|
||||
|
||||
/**
|
||||
* @description 现存地址列表
|
||||
*/
|
||||
existAddress: makeArrayProp<AddressExistRegionData>([]),
|
||||
|
||||
/**
|
||||
* @description 已有地址标题
|
||||
*/
|
||||
existAddressTitle: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 切换自定义地址和已有地址的按钮标题
|
||||
*/
|
||||
customAndExistTitle: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 弹层中内容容器的高度
|
||||
*/
|
||||
height: makeNumericProp('200'),
|
||||
|
||||
/**
|
||||
* @description 列提示文字
|
||||
*/
|
||||
columnsPlaceholder: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
}
|
||||
|
||||
export type AddressProps = ExtractPropTypes<typeof addressProps>
|
||||
|
||||
export const addressEmits = {
|
||||
[UPDATE_VISIBLE_EVENT]: (val: boolean) => isBoolean(val),
|
||||
[UPDATE_MODEL_EVENT]: () => true,
|
||||
[CLOSE_EVENT]: (val: {
|
||||
data: any
|
||||
type: string
|
||||
}) => val instanceof Object,
|
||||
[CHANGE_EVENT]: (val: {
|
||||
next?: string
|
||||
value?: AddressRegionData
|
||||
custom: string
|
||||
}) => val instanceof Object,
|
||||
switchModule: (val: { type: AddressType }) => val instanceof Object,
|
||||
closeMask: (val: { closeWay: 'self' | 'mask' | 'cross' }) => val instanceof Object,
|
||||
[SELECTED_EVENT]: (prevExistAdd: AddressExistRegionData, item: AddressExistRegionData, copyExistAdd: AddressExistRegionData[]) => prevExistAdd instanceof Object && item instanceof Object && copyExistAdd instanceof Object,
|
||||
|
||||
}
|
||||
|
||||
export type AddressEmits = typeof addressEmits
|
||||
443
uni_modules/nutui-uni/components/address/address.vue
Normal file
443
uni_modules/nutui-uni/components/address/address.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
|
||||
import { computed, defineComponent, reactive, ref, watch } from 'vue'
|
||||
import { CHANGE_EVENT, CLOSE_EVENT, PREFIX, SELECTED_EVENT, UPDATE_MODEL_EVENT, UPDATE_VISIBLE_EVENT } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import requestAniFrame from '../_utils/raf'
|
||||
import { useTranslate } from '../../locale'
|
||||
import NutElevator from '../elevator/elevator.vue'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import { addressEmits, addressProps } from './address'
|
||||
import type { AddressExistRegionData, AddressRegionData, CustomRegionData } from './type'
|
||||
|
||||
const props = defineProps(addressProps)
|
||||
const emit = defineEmits(addressEmits)
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const showPopup = ref(props.visible)
|
||||
const privateType = ref(props.type)
|
||||
const tabIndex = ref(0)
|
||||
const prevTabIndex = ref(0)
|
||||
const tabName = ref(['province', 'city', 'country', 'town'])
|
||||
const scrollDis = ref([0, 0, 0, 0])
|
||||
const scrollTop = ref(0)
|
||||
const regionData = reactive<Array<AddressRegionData[]>>([])
|
||||
|
||||
const regionList = computed(() => {
|
||||
switch (tabIndex.value) {
|
||||
case 0:
|
||||
return props.province
|
||||
case 1:
|
||||
return props.city
|
||||
case 2:
|
||||
return props.country
|
||||
default:
|
||||
return props.town
|
||||
}
|
||||
})
|
||||
|
||||
function transformData(data: AddressRegionData[]) {
|
||||
if (!Array.isArray(data))
|
||||
throw new TypeError('params muse be array.')
|
||||
|
||||
if (!data.length)
|
||||
return []
|
||||
|
||||
data.forEach((item: AddressRegionData) => {
|
||||
if (!item.title)
|
||||
console.warn('[NutUI] <Address> 请检查数组选项的 title 值是否有设置 ,title 为必填项 .')
|
||||
})
|
||||
|
||||
const newData: CustomRegionData[] = []
|
||||
|
||||
data = data.sort((a: AddressRegionData, b: AddressRegionData) => {
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
|
||||
data.forEach((item: AddressRegionData) => {
|
||||
const index = newData.findIndex((value: CustomRegionData) => value.title === item.title)
|
||||
if (index <= -1) {
|
||||
newData.push({
|
||||
title: item.title,
|
||||
list: ([] as any).concat(item),
|
||||
})
|
||||
}
|
||||
else {
|
||||
newData[index].list.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
const selectedRegion = ref<AddressRegionData[]>([])
|
||||
|
||||
let selectedExistAddress = reactive({}) // 当前选择的地址
|
||||
|
||||
const closeWay = ref<'self' | 'mask' | 'cross'>('self')
|
||||
|
||||
// 设置选中省市县
|
||||
function initCustomSelected() {
|
||||
regionData[0] = props.province || []
|
||||
regionData[1] = props.city || []
|
||||
regionData[2] = props.country || []
|
||||
regionData[3] = props.town || []
|
||||
|
||||
const defaultValue = props.modelValue
|
||||
const num = defaultValue.length
|
||||
if (num > 0) {
|
||||
tabIndex.value = num - 1
|
||||
if (regionList.value.length === 0) {
|
||||
tabIndex.value = 0
|
||||
return
|
||||
}
|
||||
for (let index = 0; index < num; index++) {
|
||||
const arr: AddressRegionData[] = regionData[index]
|
||||
selectedRegion.value[index] = arr.filter((item: AddressRegionData) => item.id === defaultValue[index])[0]
|
||||
}
|
||||
scrollTo()
|
||||
}
|
||||
}
|
||||
|
||||
function getTabName(item: AddressRegionData | null, index: number) {
|
||||
if (item && item.name)
|
||||
return item.name
|
||||
if (tabIndex.value < index && item)
|
||||
return item.name
|
||||
|
||||
else
|
||||
return props.columnsPlaceholder[index] || translate('select')
|
||||
}
|
||||
|
||||
// 手动关闭 点击叉号(cross),或者蒙层(mask)
|
||||
function handClose(type = 'self') {
|
||||
closeWay.value = type === 'cross' ? 'cross' : 'self'
|
||||
|
||||
showPopup.value = false
|
||||
}
|
||||
|
||||
// 点击遮罩层关闭
|
||||
function clickOverlay() {
|
||||
closeWay.value = 'mask'
|
||||
}
|
||||
|
||||
// 切换下一级列表
|
||||
function nextAreaList(item: AddressRegionData) {
|
||||
const tab = tabIndex.value
|
||||
prevTabIndex.value = tabIndex.value
|
||||
const callBackParams: {
|
||||
next?: string
|
||||
value?: AddressRegionData
|
||||
custom: string
|
||||
} = {
|
||||
custom: tabName.value[tab],
|
||||
}
|
||||
|
||||
selectedRegion.value[tab] = item
|
||||
|
||||
// 删除右边已选择数据
|
||||
selectedRegion.value.splice(tab + 1, selectedRegion.value.length - (tab + 1))
|
||||
|
||||
callBackParams.value = item
|
||||
|
||||
if (regionData[tab + 1]?.length > 0) {
|
||||
tabIndex.value = tab + 1
|
||||
|
||||
callBackParams.next = tabName.value[tabIndex.value]
|
||||
|
||||
scrollToTop()
|
||||
}
|
||||
else {
|
||||
handClose()
|
||||
emit(UPDATE_MODEL_EVENT)
|
||||
}
|
||||
emit(CHANGE_EVENT, callBackParams)
|
||||
}
|
||||
// 切换地区Tab
|
||||
function changeRegionTab(item: AddressRegionData, index: number) {
|
||||
prevTabIndex.value = tabIndex.value
|
||||
if (getTabName(item, index)) {
|
||||
tabIndex.value = index
|
||||
scrollTo()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollChange(e: ScrollViewOnScrollEvent) {
|
||||
scrollDis.value[tabIndex.value] = e.detail.scrollTop
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
// scrollTop 不会实时变更。当再次赋值时,scrollTop无变化时,不会触发滚动
|
||||
scrollTop.value += 1
|
||||
requestAniFrame(() => {
|
||||
setTimeout(() => {
|
||||
// 直接设置为0无效
|
||||
scrollTop.value = 0.01
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function scrollTo() {
|
||||
// scrollTop 不会实时变更。当再次赋值时,scrollTop无变化时,不会触发滚动
|
||||
scrollTop.value += 1
|
||||
requestAniFrame(() => {
|
||||
setTimeout(() => {
|
||||
scrollTop.value = scrollDis.value[tabIndex.value]
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
// 选择现有地址
|
||||
function selectedExist(item: AddressExistRegionData) {
|
||||
const copyExistAdd = props.existAddress
|
||||
let prevExistAdd: AddressExistRegionData = {} as AddressExistRegionData
|
||||
|
||||
copyExistAdd.forEach((list: AddressExistRegionData) => {
|
||||
if (list && list.selectedAddress)
|
||||
prevExistAdd = list
|
||||
list.selectedAddress = false
|
||||
})
|
||||
|
||||
item.selectedAddress = true
|
||||
|
||||
selectedExistAddress = item
|
||||
|
||||
emit(SELECTED_EVENT, prevExistAdd, item, copyExistAdd)
|
||||
|
||||
handClose()
|
||||
}
|
||||
// 初始化
|
||||
function initAddress() {
|
||||
selectedRegion.value = []
|
||||
tabIndex.value = 0
|
||||
scrollTo()
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function close() {
|
||||
const data = {
|
||||
addressIdStr: '',
|
||||
addressStr: '',
|
||||
province: selectedRegion.value[0],
|
||||
city: selectedRegion.value[1],
|
||||
country: selectedRegion.value[2],
|
||||
town: selectedRegion.value[3],
|
||||
}
|
||||
|
||||
const callBackParams = {
|
||||
data: {},
|
||||
type: privateType.value,
|
||||
}
|
||||
|
||||
if (['custom', 'custom2'].includes(privateType.value)) {
|
||||
[0, 1, 2, 3].forEach((i) => {
|
||||
const item = selectedRegion.value[i]
|
||||
data.addressIdStr += `${i ? '_' : ''}${(item && item.id) || 0}`
|
||||
data.addressStr += (item && item.name) || ''
|
||||
})
|
||||
|
||||
callBackParams.data = data
|
||||
}
|
||||
else {
|
||||
callBackParams.data = selectedExistAddress
|
||||
}
|
||||
|
||||
initAddress()
|
||||
|
||||
if (closeWay.value === 'self')
|
||||
emit(CLOSE_EVENT, callBackParams)
|
||||
else
|
||||
emit('closeMask', { closeWay: closeWay.value })
|
||||
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
|
||||
// 选择其他地址
|
||||
function switchModule() {
|
||||
const type = privateType.value
|
||||
privateType.value = type === 'exist' ? 'custom' : 'exist'
|
||||
initAddress()
|
||||
emit('switchModule', { type: privateType.value })
|
||||
}
|
||||
|
||||
function handleElevatorItem(key: string, item: AddressRegionData) {
|
||||
nextAreaList(item)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
showPopup.value = value
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => showPopup.value,
|
||||
(value) => {
|
||||
if (value)
|
||||
initCustomSelected()
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-address`
|
||||
const { translate } = useTranslate(componentName)
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NutPopup
|
||||
v-model:visible="showPopup"
|
||||
:z-index="zIndex"
|
||||
position="bottom"
|
||||
:lock-scroll="lockScroll"
|
||||
:round="round"
|
||||
@close="close"
|
||||
@click-overlay="clickOverlay"
|
||||
@open="closeWay = 'self'"
|
||||
>
|
||||
<view :class="classes" :style="customStyle">
|
||||
<view class="nut-address__header">
|
||||
<view class="nut-address__header-back" @click="switchModule">
|
||||
<slot v-if="type === 'exist' && privateType === 'custom'" name="backIcon">
|
||||
<NutIcon name="left" size="14px" />
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<view class="nut-address__header__title">
|
||||
{{
|
||||
privateType === 'custom'
|
||||
? customAddressTitle || translate('selectRegion')
|
||||
: existAddressTitle || translate('deliveryTo')
|
||||
}}
|
||||
</view>
|
||||
|
||||
<view class="nut-address__header-close" @click="handClose('cross')">
|
||||
<slot name="closeIcon">
|
||||
<NutIcon name="close" custom-color="#cccccc" size="14px" />
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 请选择 -->
|
||||
<view v-if="['custom', 'custom2'].includes(privateType)" class="nut-address__custom">
|
||||
<view class="nut-address__region">
|
||||
<view
|
||||
v-for="(item, index) in selectedRegion"
|
||||
:key="index"
|
||||
class="nut-address__region-item "
|
||||
:class="[index === tabIndex ? 'active' : '']"
|
||||
@click="changeRegionTab(item, index)"
|
||||
>
|
||||
<view>{{ getTabName(item, index) }} </view>
|
||||
<view class="nut-address__region-line--mini" :class="{ active: index === tabIndex }" />
|
||||
</view>
|
||||
<view v-if="tabIndex === selectedRegion.length" class="active nut-address__region-item">
|
||||
<view>{{ getTabName(null, selectedRegion.length) }} </view>
|
||||
<view class="nut-address__region-line--mini active" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="privateType === 'custom'" class="nut-address__detail">
|
||||
<div class="nut-address__detail-list">
|
||||
<scroll-view
|
||||
:scroll-y="true"
|
||||
:style="{ height: '100%' }"
|
||||
:scroll-top="scrollTop"
|
||||
@scroll="scrollChange"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in regionList"
|
||||
:key="index"
|
||||
class="nut-address__detail-item"
|
||||
:class="[selectedRegion[tabIndex]?.id === item.id ? 'active' : '']"
|
||||
@click="nextAreaList(item)"
|
||||
>
|
||||
<view>
|
||||
<slot v-if="selectedRegion[tabIndex]?.id === item.id" name="icon">
|
||||
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>{{ item.name }}
|
||||
</view>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</view>
|
||||
|
||||
<view v-else class="nut-address__elevator-group">
|
||||
<NutElevator
|
||||
:height="height"
|
||||
:index-list="transformData(regionList)"
|
||||
@click-item="handleElevatorItem"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 配送至 -->
|
||||
<view v-else-if="privateType === 'exist'" class="nut-address__exist">
|
||||
<div class="nut-address__exist-group">
|
||||
<ul class="nut-address__exist-group-list">
|
||||
<li
|
||||
v-for="(item, index) in existAddress"
|
||||
:key="index"
|
||||
class="nut-address__exist-group-item"
|
||||
:class="[item.selectedAddress ? 'active' : '']"
|
||||
@click="selectedExist(item)"
|
||||
>
|
||||
<slot v-if="!item.selectedAddress" name="unselectedIcon">
|
||||
<NutIcon name="location2" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>
|
||||
|
||||
<slot v-if="item.selectedAddress" name="icon">
|
||||
<NutIcon name="Check" custom-class="nut-address-select-icon" width="13px" />
|
||||
</slot>
|
||||
|
||||
<div class="nut-address__exist-item-info">
|
||||
<div v-if="item.name && item.phone" class="nut-address__exist-item-info-top">
|
||||
<div class="nut-address__exist-item-info-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="nut-address__exist-item-info-phone">
|
||||
{{ item.phone }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nut-address__exist-item-info-bottom">
|
||||
<view>
|
||||
{{ item.provinceName + item.cityName + item.countyName + item.townName + item.addressDetail }}
|
||||
</view>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="isShowCustomAddress" class="nut-address__exist-choose" @click="switchModule">
|
||||
<div class="nut-address__exist-choose-btn">
|
||||
{{
|
||||
customAndExistTitle || translate('chooseAnotherAddress')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="!isShowCustomAddress">
|
||||
<slot name="bottom" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</NutPopup>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
232
uni_modules/nutui-uni/components/address/index.scss
Normal file
232
uni_modules/nutui-uni/components/address/index.scss
Normal file
@@ -0,0 +1,232 @@
|
||||
@import '../popup/index';
|
||||
@import '../elevator/index';
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-address {
|
||||
&__header {
|
||||
color: $dark-color;
|
||||
|
||||
&__title {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__custom {
|
||||
.nut-address__region {
|
||||
color: $dark-color;
|
||||
}
|
||||
|
||||
.nut-address__detail {
|
||||
.nut-address__detail-list {
|
||||
.nut-address__detail-item {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__exist {
|
||||
.nut-address__exist-group {
|
||||
.nut-address__exist-group-list {
|
||||
.nut-address__exist-group-item {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__exist-choose {
|
||||
border-top: 1px solid $dark-background;
|
||||
}
|
||||
}
|
||||
|
||||
&-custom-buttom {
|
||||
border-top: 1px solid $dark-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address {
|
||||
display: block;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 68px;
|
||||
padding: 0 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: $address-header-title-font-size;
|
||||
color: $address-header-title-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 请选择
|
||||
.nut-address__custom {
|
||||
display: block;
|
||||
|
||||
.nut-address__region {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
// margin-top: 32px;
|
||||
padding: 0 20px;
|
||||
font-size: $address-region-tab-font-size;
|
||||
color: $address-region-tab-color;
|
||||
|
||||
.nut-address__region-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-width: 2px;
|
||||
margin-right: 30px;
|
||||
|
||||
&.active {
|
||||
font-weight: $address-region-tab-active-item-font-weight;
|
||||
}
|
||||
|
||||
view {
|
||||
display: block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nut-address__region-line--mini {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
margin-top: 5px;
|
||||
background: $address-region-tab-line;
|
||||
transition: 0.2s all linear;
|
||||
|
||||
&.active {
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__region-line {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 20px;
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 3px;
|
||||
margin-top: 5px;
|
||||
background: $address-region-tab-line;
|
||||
border-radius: $address-region-tab-line-border-radius;
|
||||
opacity: $address-region-tab-line-opacity;
|
||||
transition: 0.2s all linear;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__detail {
|
||||
display: block;
|
||||
margin: 20px 20px 0;
|
||||
|
||||
.nut-address__detail-list {
|
||||
// overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
height: 270px;
|
||||
padding: 0;
|
||||
padding-top: 15px;
|
||||
|
||||
.nut-address__detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: $address-region-item-font-size;
|
||||
color: $address-region-item-color;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__elevator-group {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// 配送至
|
||||
.nut-address__exist {
|
||||
display: block;
|
||||
margin-top: 15px;
|
||||
|
||||
.nut-address__exist-group {
|
||||
height: 279px;
|
||||
padding: 15px 20px 0;
|
||||
overflow-y: scroll;
|
||||
|
||||
.nut-address__exist-group-list {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
|
||||
.nut-address__exist-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: $font-size-1;
|
||||
line-height: 14px;
|
||||
color: #333;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.exist-item-icon {
|
||||
margin-right: $address-item-margin-right;
|
||||
color: $address-icon-color !important;
|
||||
}
|
||||
|
||||
// span {
|
||||
// display: inline-block;
|
||||
// flex: 1;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address__exist-choose {
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
padding: 6px 0 0;
|
||||
border-top: 1px solid #f2f2f2;
|
||||
|
||||
.nut-address__exist-choose-btn {
|
||||
width: 90%;
|
||||
height: 42px;
|
||||
margin: auto;
|
||||
font-size: 15px;
|
||||
line-height: 42px;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
background: $button-primary-background-color;
|
||||
border-radius: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-select-icon {
|
||||
margin-right: $address-item-margin-right;
|
||||
color: $address-icon-color !important;
|
||||
}
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/address/index.ts
Normal file
2
uni_modules/nutui-uni/components/address/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './address'
|
||||
export * from './type'
|
||||
23
uni_modules/nutui-uni/components/address/type.ts
Normal file
23
uni_modules/nutui-uni/components/address/type.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface AddressRegionData {
|
||||
name: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface CustomRegionData {
|
||||
title: string
|
||||
list: any[]
|
||||
}
|
||||
|
||||
export interface AddressExistRegionData {
|
||||
id?: string | number
|
||||
provinceName: string
|
||||
cityName: string
|
||||
countyName: string
|
||||
townName: string
|
||||
addressDetail: string
|
||||
selectedAddress: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const addressType = ['exist', 'custom', 'custom2'] as const
|
||||
export type AddressType = (typeof addressType)[number]
|
||||
42
uni_modules/nutui-uni/components/addresslist/addresslist.ts
Normal file
42
uni_modules/nutui-uni/components/addresslist/addresslist.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, isNumber, isString, makeArrayProp, makeObjectProp, truthProp } from '../_utils'
|
||||
import type { AddressListOptions } from './type'
|
||||
|
||||
export const addresslistProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 地址数组
|
||||
*/
|
||||
data: makeArrayProp<any>([]),
|
||||
/**
|
||||
* @description 长按功能
|
||||
*/
|
||||
longPress: Boolean,
|
||||
/**
|
||||
* @description 右滑功能
|
||||
*/
|
||||
swipeEdition: Boolean,
|
||||
/**
|
||||
* @description 是否展示底部按钮
|
||||
*/
|
||||
showBottomButton: truthProp,
|
||||
/**
|
||||
* @description 自定义 `key` 值时,设置映射关系
|
||||
*/
|
||||
options: makeObjectProp<AddressListOptions>({}),
|
||||
}
|
||||
|
||||
export type AddressListProps = ExtractPropTypes<typeof addresslistProps>
|
||||
|
||||
export const addresslistEmits = {
|
||||
delIcon: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
editIcon: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
clickItem: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
longCopy: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
longSet: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
longDel: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
swipeDel: (val: Event, item: unknown, index: number | string) => (val instanceof Object) && (item instanceof Object) && (isNumber(index) || isString(index)),
|
||||
add: (val: Event) => val instanceof Object,
|
||||
}
|
||||
|
||||
export type AddressListEmits = typeof addresslistEmits
|
||||
165
uni_modules/nutui-uni/components/addresslist/addresslist.vue
Normal file
165
uni_modules/nutui-uni/components/addresslist/addresslist.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, onMounted, reactive, ref, useSlots, watch } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { floatData, getMainClass } from '../_utils'
|
||||
import { useTranslate } from '../../locale'
|
||||
import NutButton from '../button/button.vue'
|
||||
import { addresslistEmits, addresslistProps } from './addresslist'
|
||||
import GeneralShell from './compoents/generalshell.vue'
|
||||
|
||||
const props = defineProps(addresslistProps)
|
||||
|
||||
const emit = defineEmits(addresslistEmits)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
function hasSlot(name: string) {
|
||||
return Boolean(slots[name])
|
||||
}
|
||||
|
||||
const dataArray = ref<any[]>([])
|
||||
const dataInfo = reactive({
|
||||
id: 2,
|
||||
addressName: '姓名',
|
||||
phone: '123****4567',
|
||||
defaultAddress: false,
|
||||
fullAddress: '北京市通州区测试测试测试测试测试测试测试测试测试',
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
// 磨平参数差异
|
||||
function trowelData() {
|
||||
if (Object.keys(props.options).length > 0) {
|
||||
dataArray.value = props.data.map((item) => {
|
||||
return floatData(dataInfo, item, props.options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => trowelData(),
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
function handleDelIconClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('delIcon', event, item, index)
|
||||
}
|
||||
|
||||
function handleEditIconClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('editIcon', event, item, index)
|
||||
}
|
||||
|
||||
function handleContentItemClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('clickItem', event, item, index)
|
||||
}
|
||||
|
||||
function handleLongCopyClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longCopy', event, item, index)
|
||||
}
|
||||
|
||||
function handleLongSetClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longSet', event, item, index)
|
||||
}
|
||||
|
||||
function handleLongDelClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longDel', event, item, index)
|
||||
}
|
||||
|
||||
function handleSwipeDelClick(event: any, item: any, index: number | string) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('swipeDel', event, item, index)
|
||||
}
|
||||
|
||||
function handleAddressAdd(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('add', event)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
trowelData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-address-list`
|
||||
const { translate } = useTranslate(componentName)
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
// #ifndef H5
|
||||
styleIsolation: 'shared',
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="customStyle">
|
||||
<GeneralShell
|
||||
v-for="(item, index) in dataArray"
|
||||
:key="index"
|
||||
:address="item"
|
||||
:long-press="props.longPress"
|
||||
:swipe-edition="props.swipeEdition"
|
||||
:use-content-info-slot="hasSlot('itemInfos')"
|
||||
:use-content-icons-slot="hasSlot('itemIcon')"
|
||||
:use-content-addrs-slot="hasSlot('itemAddr')"
|
||||
:use-longpress-all-slot="hasSlot('longpressBtns')"
|
||||
:use-swipe-right-btn-slot="hasSlot('swipeRight')"
|
||||
@del-icon="handleDelIconClick($event, item, index)"
|
||||
@edit-icon="handleEditIconClick($event, item, index)"
|
||||
@click-item="handleContentItemClick($event, item, index)"
|
||||
@swipe-del="handleSwipeDelClick($event, item, index)"
|
||||
@long-copy="handleLongCopyClick($event, item, index)"
|
||||
@long-set="handleLongSetClick($event, item, index)"
|
||||
@long-del="handleLongDelClick($event, item, index)"
|
||||
>
|
||||
<template #content-info>
|
||||
<slot name="itemInfos" :item="item" />
|
||||
</template>
|
||||
<template #content-icons>
|
||||
<slot name="itemIcon" :item="item" />
|
||||
</template>
|
||||
<template #content-addrs>
|
||||
<slot name="itemAddr" :item="item" />
|
||||
</template>
|
||||
<template v-if="props.longPress" #longpress-all>
|
||||
<slot name="longpressBtns" :item="item" />
|
||||
</template>
|
||||
<template v-if="props.swipeEdition" #swipe-right-btn>
|
||||
<slot name="swipeRight" :item="item" />
|
||||
</template>
|
||||
</GeneralShell>
|
||||
|
||||
<view v-if="props.showBottomButton" class="nut-address-list__bottom" @click="handleAddressAdd">
|
||||
<NutButton type="danger" block>
|
||||
{{ translate('addAddress') }}
|
||||
</NutButton>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue'
|
||||
import { PREFIX } from '../../_constants'
|
||||
import { useTranslate } from '../../../locale'
|
||||
import NutIcon from '../../icon/icon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
useContentTopSlot: Boolean,
|
||||
useContentIconSlot: Boolean,
|
||||
useContentAddrSlot: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['delIcon', 'editIcon', 'clickItem'])
|
||||
|
||||
function handleDelIconClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('delIcon', event, props.item)
|
||||
}
|
||||
|
||||
function handleEditIconClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('editIcon', event, props.item)
|
||||
}
|
||||
|
||||
function handleContentsClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('clickItem', event, props.item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-address-list-item`
|
||||
const { translate } = useTranslate(`${PREFIX}-address-list`)
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
// #ifndef H5
|
||||
styleIsolation: 'shared',
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="nut-address-list-item" @click="handleContentsClick">
|
||||
<view class="nut-address-list-item__info">
|
||||
<view class="nut-address-list-item__info-contact">
|
||||
<slot v-if="props.useContentTopSlot" name="content-top" />
|
||||
|
||||
<template v-else>
|
||||
<view class="nut-address-list-item__info-contact-name">
|
||||
{{ props.item.addressName }}
|
||||
</view>
|
||||
<view class="nut-address-list-item__info-contact-tel">
|
||||
{{ props.item.phone }}
|
||||
</view>
|
||||
<view v-if="props.item.defaultAddress" class="nut-address-list-item__info-contact-default">
|
||||
{{ translate('default') }}
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="nut-address-list-item__info-handle">
|
||||
<slot v-if="props.useContentIconSlot" name="content-icon" />
|
||||
|
||||
<template v-else>
|
||||
<NutIcon name="del" custom-class="nut-address-list-item__info-handle-del" @tap.stop="handleDelIconClick" />
|
||||
<NutIcon name="edit" custom-class="nut-address-list-item__info-handle-edit" @tap.stop="handleEditIconClick" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="nut-address-list-item__addr">
|
||||
<slot v-if="props.useContentAddrSlot" name="content-addr" />
|
||||
|
||||
<template v-else>
|
||||
{{ props.item.fullAddress }}
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.nut-theme-dark {
|
||||
.nut-address-list {
|
||||
&-item {
|
||||
&__addr {
|
||||
color: $dark-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address-list {
|
||||
&-item {
|
||||
width: 100%;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-contact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-weight: bold;
|
||||
|
||||
&-name {
|
||||
max-width: 145px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&-tel {
|
||||
max-width: 110px;
|
||||
margin-left: 8px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&-default {
|
||||
height: 16px;
|
||||
padding: 0 6px;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $addresslist-contnts-contact-color;
|
||||
background: $addresslist-contnts-contact-default;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&-handle {
|
||||
&-edit {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__addr {
|
||||
margin-top: 5px;
|
||||
font-size: $addresslist-addr-font-size;
|
||||
color: $addresslist-addr-font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,335 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { PREFIX } from '../../_constants'
|
||||
import NutButton from '../../button/button.vue'
|
||||
import NutSwipe from '../../swipe/swipe.vue'
|
||||
import ItemContents from './Itemcontents.vue'
|
||||
|
||||
const props = defineProps({
|
||||
address: {
|
||||
type: Object,
|
||||
},
|
||||
longPress: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
swipeEdition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
useContentInfoSlot: Boolean,
|
||||
useContentIconsSlot: Boolean,
|
||||
useContentAddrsSlot: Boolean,
|
||||
useLongpressAllSlot: Boolean,
|
||||
useSwipeRightBtnSlot: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['delIcon', 'editIcon', 'clickItem', 'longDown', 'longCopy', 'longSet', 'longDel', 'swipeDel'])
|
||||
|
||||
const moveRef = ref<boolean>(false)
|
||||
const showMaskRef = ref<boolean>(false)
|
||||
|
||||
function handleDelIconClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('delIcon', event, props.address)
|
||||
}
|
||||
|
||||
function handleEditIconClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('editIcon', event, props.address)
|
||||
}
|
||||
|
||||
function handleItemClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
if (moveRef.value)
|
||||
return
|
||||
|
||||
emit('clickItem', event, props.address)
|
||||
}
|
||||
|
||||
function handleLongDelClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longDel', event, props.address)
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
|
||||
function destroyTimer() {
|
||||
if (timer == null)
|
||||
return
|
||||
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
function startTimer(event: any) {
|
||||
timer = setTimeout(() => {
|
||||
showMaskRef.value = true
|
||||
|
||||
emit('longDown', event, props.address)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 长按功能实现
|
||||
function handleTouchStart(event: any) {
|
||||
startTimer(event)
|
||||
}
|
||||
|
||||
function handleTouchMove() {
|
||||
// 滑动不触发长按
|
||||
destroyTimer()
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
// 删除定时器,防止重复注册
|
||||
destroyTimer()
|
||||
}
|
||||
|
||||
function handleHideMaskClick() {
|
||||
showMaskRef.value = false
|
||||
}
|
||||
|
||||
function handleLongCopyClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longCopy', event, props.address)
|
||||
}
|
||||
|
||||
function handleLongSetClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('longSet', event, props.address)
|
||||
}
|
||||
|
||||
function handleMaskClick(event: any) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
if (timer != null) {
|
||||
// 排除长按时触发点击的情况
|
||||
showMaskRef.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwipeDelClick(event: any) {
|
||||
event.stopPropagation()
|
||||
|
||||
emit('swipeDel', event, props.address)
|
||||
}
|
||||
|
||||
function handleSwipeStart() {
|
||||
moveRef.value = false
|
||||
}
|
||||
|
||||
function handleSwipeMove() {
|
||||
moveRef.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-address-list-general`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
// #ifndef H5
|
||||
styleIsolation: 'shared',
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="!props.swipeEdition" class="nut-address-list-general">
|
||||
<ItemContents
|
||||
:item="props.address"
|
||||
:use-content-top-slot="props.useContentInfoSlot"
|
||||
:use-content-icon-slot="props.useContentIconsSlot"
|
||||
:use-content-addr-slot="props.useContentAddrsSlot"
|
||||
@del-icon="handleDelIconClick"
|
||||
@edit-icon="handleEditIconClick"
|
||||
@click-item="handleItemClick"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<template #content-top>
|
||||
<slot name="content-info" />
|
||||
</template>
|
||||
<template #content-icon>
|
||||
<slot name="content-icons" />
|
||||
</template>
|
||||
<template #content-addr>
|
||||
<slot name="content-addrs" />
|
||||
</template>
|
||||
</ItemContents>
|
||||
|
||||
<view v-if="props.longPress && showMaskRef" class="nut-address-list-general__mask" @click="handleMaskClick">
|
||||
<slot v-if="props.useLongpressAllSlot" name="longpress-all" />
|
||||
|
||||
<template v-else>
|
||||
<view class="nut-address-list-general__mask-copy" @click="handleLongCopyClick">
|
||||
复制地址
|
||||
</view>
|
||||
<view class="nut-address-list-general__mask-set" @click="handleLongSetClick">
|
||||
设置默认
|
||||
</view>
|
||||
<view class="nut-address-list-general__mask-del" @click="handleLongDelClick">
|
||||
删除地址
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view v-if="showMaskRef" class="nut-address-list__mask-bottom" @click="handleHideMaskClick" />
|
||||
</view>
|
||||
|
||||
<NutSwipe v-else>
|
||||
<view class="nut-address-list-swipe">
|
||||
<ItemContents
|
||||
:item="props.address"
|
||||
:use-content-top-slot="props.useContentInfoSlot"
|
||||
:use-content-icon-slot="props.useContentIconsSlot"
|
||||
:use-content-addr-slot="props.useContentAddrsSlot"
|
||||
@del-icon="handleDelIconClick"
|
||||
@edit-icon="handleEditIconClick"
|
||||
@click-item="handleItemClick"
|
||||
@touchstart="handleSwipeStart"
|
||||
@touchmove="handleSwipeMove"
|
||||
>
|
||||
<template #content-top>
|
||||
<slot name="content-info" />
|
||||
</template>
|
||||
<template #content-icon>
|
||||
<slot name="content-icons" />
|
||||
</template>
|
||||
<template #content-addr>
|
||||
<slot name="content-addrs" />
|
||||
</template>
|
||||
</ItemContents>
|
||||
</view>
|
||||
|
||||
<template #right>
|
||||
<view style="height: 100%;">
|
||||
<slot v-if="props.useSwipeRightBtnSlot" name="swipe-right-btn" />
|
||||
|
||||
<template v-else>
|
||||
<NutButton
|
||||
shape="square"
|
||||
custom-style="height: 100%;"
|
||||
type="danger"
|
||||
@tap.stop="handleSwipeDelClick"
|
||||
>
|
||||
删除
|
||||
</NutButton>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
</NutSwipe>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.nut-theme-dark {
|
||||
.nut-address-list {
|
||||
&-swipe,
|
||||
&-general {
|
||||
color: $dark-color;
|
||||
background-color: $dark-background2;
|
||||
border-bottom: 1px solid $dark-color-gray;
|
||||
|
||||
&__mask {
|
||||
background-color: $dark-color3;
|
||||
|
||||
&-copy {
|
||||
color: $dark-color-gray;
|
||||
background-color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address-list {
|
||||
&-swipe,
|
||||
&-general {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 76px;
|
||||
padding: 5px 10px;
|
||||
font-size: $addresslist-font-size;
|
||||
color: $addresslist-font-color;
|
||||
background-color: $addresslist-bg;
|
||||
border-bottom: 1px solid $addresslist-border;
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 0 40px;
|
||||
background-color: $addresslist-mask-bg;
|
||||
|
||||
&-copy,
|
||||
&-set,
|
||||
&-del {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: $white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-set {
|
||||
color: $white;
|
||||
background-color: $addresslist-set-bg;
|
||||
}
|
||||
|
||||
&-del {
|
||||
color: $white;
|
||||
background-color: $addresslist-del-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-general {
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-swipe {
|
||||
&:last-of-type {
|
||||
.nut-address-list-swipe {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address-list__mask-bottom {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
uni_modules/nutui-uni/components/addresslist/index.scss
Normal file
42
uni_modules/nutui-uni/components/addresslist/index.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
@import "../button/index";
|
||||
@import "../swipe/index";
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-address-list {
|
||||
&__bottom {
|
||||
background-color: $dark-background2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-address-list {
|
||||
overflow: hidden;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 84px;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
bottom: constant(safe-area-inset-bottom);
|
||||
bottom: env(safe-area-inset-bottom);
|
||||
left: 0;
|
||||
z-index: 100000;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 12px 18px 24px;
|
||||
background-color: $addresslist-bg;
|
||||
}
|
||||
|
||||
.nut-address-list__mask-bottom {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/addresslist/index.ts
Normal file
2
uni_modules/nutui-uni/components/addresslist/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './addresslist'
|
||||
export * from './type'
|
||||
3
uni_modules/nutui-uni/components/addresslist/type.ts
Normal file
3
uni_modules/nutui-uni/components/addresslist/type.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface AddressListOptions {
|
||||
[key: string]: string
|
||||
}
|
||||
44
uni_modules/nutui-uni/components/animate/animate.ts
Normal file
44
uni_modules/nutui-uni/components/animate/animate.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CLICK_EVENT } from '../_constants'
|
||||
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
|
||||
import type { AnimateAction, AnimateType } from './type'
|
||||
|
||||
export const animateProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 控制动画,当值从 false 变为 true 时会触发一次动画
|
||||
*/
|
||||
show: Boolean,
|
||||
|
||||
/**
|
||||
* @description 动画类型
|
||||
* @values 'fade', 'slide', 'zoom', ...
|
||||
*/
|
||||
type: makeStringProp<AnimateType | ''>(''),
|
||||
|
||||
/**
|
||||
* @description 是否循环执行。`true`-循环执行; `false`-执行一次
|
||||
*/
|
||||
loop: Boolean,
|
||||
|
||||
/**
|
||||
* @description 动画时长,单位 ms
|
||||
*/
|
||||
duration: makeNumericProp(500),
|
||||
|
||||
/**
|
||||
* @description (不能与 show 同时使用)触发方式,`initial`-初始化执行; `click`-点击执行
|
||||
* @values 'initial', 'click'
|
||||
* @default initial
|
||||
*/
|
||||
action: makeStringProp<AnimateAction>('initial'),
|
||||
}
|
||||
|
||||
export type AnimateProps = ExtractPropTypes<typeof animateProps>
|
||||
|
||||
export const animateEmits = {
|
||||
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
|
||||
animate: () => true,
|
||||
}
|
||||
|
||||
export type AnimateEmits = typeof animateEmits
|
||||
82
uni_modules/nutui-uni/components/animate/animate.vue
Normal file
82
uni_modules/nutui-uni/components/animate/animate.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, ref, watch } from 'vue'
|
||||
import { CLICK_EVENT, PREFIX } from '../_constants'
|
||||
import { getMainClass, getMainStyle } from '../_utils'
|
||||
import requestAniFrame from '../_utils/raf'
|
||||
import { animateEmits, animateProps } from './animate'
|
||||
|
||||
const props = defineProps(animateProps)
|
||||
const emit = defineEmits(animateEmits)
|
||||
|
||||
const animated = ref(props.action === 'initial' || props.show === true || props.loop)
|
||||
const classes = computed(() => {
|
||||
const obj = {
|
||||
[`${componentName}__container`]: true,
|
||||
[`${componentName}-${props.type}`]: animated.value,
|
||||
loop: props.loop,
|
||||
}
|
||||
return getMainClass(props, componentName, obj)
|
||||
})
|
||||
const getStyle = computed(() => {
|
||||
return getMainStyle(props, {
|
||||
animationDuration: props.duration ? `${props.duration}ms` : undefined,
|
||||
})
|
||||
})
|
||||
|
||||
function animate() {
|
||||
animated.value = false
|
||||
// #ifdef H5
|
||||
requestAniFrame(() => {
|
||||
requestAniFrame(() => {
|
||||
animated.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleClick(event: unknown) {
|
||||
if (props.action === 'click') {
|
||||
animate()
|
||||
emit(CLICK_EVENT, event as MouseEvent)
|
||||
emit('animate')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) {
|
||||
animate()
|
||||
emit('animate')
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-animate`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="nut-animate">
|
||||
<view
|
||||
:class="classes"
|
||||
:style="getStyle"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
316
uni_modules/nutui-uni/components/animate/index.scss
Normal file
316
uni_modules/nutui-uni/components/animate/index.scss
Normal file
@@ -0,0 +1,316 @@
|
||||
.nut-animate {
|
||||
.nut-animate__container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Animation css */
|
||||
[class*="nut-animate-"] {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
// 抖动
|
||||
.nut-animate-shake {
|
||||
animation-name: shake;
|
||||
}
|
||||
|
||||
// 心跳
|
||||
.nut-animate-ripple {
|
||||
animation-name: ripple;
|
||||
}
|
||||
|
||||
// 漂浮
|
||||
.nut-animate-float {
|
||||
position: relative;
|
||||
animation-name: float-pop;
|
||||
}
|
||||
|
||||
// 呼吸灯
|
||||
.nut-animate-breath {
|
||||
animation-name: breath;
|
||||
animation-duration: 2700ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
// 右侧向左侧划入
|
||||
.nut-animate-slide-right {
|
||||
animation-name: slide-right;
|
||||
}
|
||||
|
||||
// 右侧向左侧划入
|
||||
.nut-animate-slide-left {
|
||||
animation-name: slide-left;
|
||||
}
|
||||
|
||||
// 上面向下面划入
|
||||
.nut-animate-slide-top {
|
||||
animation-name: slide-top;
|
||||
}
|
||||
|
||||
// 下面向上面划入
|
||||
.nut-animate-slide-bottom {
|
||||
animation-name: slide-bottom;
|
||||
}
|
||||
|
||||
.nut-animate-jump {
|
||||
transform-origin: center center;
|
||||
animation: jump 0.7s linear;
|
||||
}
|
||||
|
||||
// 循环
|
||||
.loop {
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
// 抖动动画
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translateX(-9px);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateX(-7px);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateX(6px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 呼吸
|
||||
@keyframes breath {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧向左侧划入
|
||||
// stylelint-disable-next-line keyframes-name-pattern
|
||||
@keyframes slide-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 左侧向右侧划入
|
||||
// stylelint-disable-next-line keyframes-name-pattern
|
||||
@keyframes slide-left {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 上面向下面划入
|
||||
// stylelint-disable-next-line keyframes-name-pattern
|
||||
@keyframes slide-top {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 下面向上面划入
|
||||
// stylelint-disable-next-line keyframes-name-pattern
|
||||
@keyframes slide-bottom {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 漂浮 float
|
||||
// stylelint-disable-next-line keyframes-name-pattern
|
||||
@keyframes float-pop {
|
||||
0% {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
25% {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
50% {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
75% {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 跳跃
|
||||
@keyframes jump {
|
||||
0% {
|
||||
transform: rotate(0deg) translateY(0);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(10deg) translateY(20 * 1px);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(0deg) translateY(-10 * 1px);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(-10deg) translateY(20 * 1px);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg) translateY(0);
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-animate-twinkle {
|
||||
position: relative;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
width: 60 * 1px;
|
||||
height: 60 * 1px;
|
||||
margin-top: calc(-30 / 2) * 1px;
|
||||
margin-right: calc(-60 / 2) * 1px;
|
||||
content: "";
|
||||
border: 4 * 1px solid rgb(255 255 255 / 60%);
|
||||
border-radius: calc(60 / 2) * 1px;
|
||||
transform: scale(0);
|
||||
animation: twinkle 2s ease-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
// 水波
|
||||
@keyframes twinkle {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
|
||||
.nut-animate-flicker {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100 * 1px;
|
||||
height: 60 * 1px;
|
||||
content: "";
|
||||
background-image: linear-gradient(106deg, rgb(232 224 255 / 0%) 24%, #e8e0ff 91%);
|
||||
filter: blur(3 * 1px);
|
||||
opacity: 0.73;
|
||||
transform: skewX(-20deg);
|
||||
animation: flicker 1.5s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% {
|
||||
transform: translateX(-100 * 1px) skewX(-20deg);
|
||||
}
|
||||
|
||||
40%,
|
||||
100% {
|
||||
transform: translateX(150 * 1px) skewX(-20deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/animate/index.ts
Normal file
1
uni_modules/nutui-uni/components/animate/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './animate'
|
||||
4
uni_modules/nutui-uni/components/animate/type.ts
Normal file
4
uni_modules/nutui-uni/components/animate/type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const animateType = ['shake', 'ripple', 'breath', 'float', 'slide-right', 'slide-left', 'slide-top', 'slide-bottom', 'jump', 'twinkle', 'flicker'] as const
|
||||
export type AnimateType = (typeof animateType)[number]
|
||||
export const animateAction = ['initial', 'click', ''] as const
|
||||
export type AnimateAction = (typeof animateAction)[number]
|
||||
25
uni_modules/nutui-uni/components/avatar/avatar.ts
Normal file
25
uni_modules/nutui-uni/components/avatar/avatar.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
|
||||
import type { AvatarShape, AvatarSize } from './type'
|
||||
|
||||
export const avatarProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 头像的大小,可选值为:`large`、`normal`、`small`,支持直接输入数字
|
||||
*/
|
||||
size: makeNumericProp<AvatarSize | string | number | undefined>(undefined),
|
||||
/**
|
||||
* @description 头像的形状,可选值为:`square`、`round`
|
||||
*/
|
||||
shape: makeStringProp<AvatarShape | undefined>(undefined),
|
||||
/**
|
||||
* @description 背景色
|
||||
*/
|
||||
bgColor: makeStringProp('#eee'),
|
||||
/**
|
||||
* @description 字体颜色
|
||||
*/
|
||||
customColor: makeStringProp('#666'),
|
||||
}
|
||||
|
||||
export type AvatarProps = ExtractPropTypes<typeof avatarProps>
|
||||
139
uni_modules/nutui-uni/components/avatar/avatar.vue
Normal file
139
uni_modules/nutui-uni/components/avatar/avatar.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { useInject } from '../_hooks'
|
||||
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
|
||||
import type { AvatarGroupProps } from '../avatargroup'
|
||||
import { AVATAR_GROUP_KEY } from '../avatargroup'
|
||||
import { avatarProps } from './avatar'
|
||||
import type { AvatarFinalSize, AvatarShape, AvatarSize } from './type'
|
||||
import { avatarSize } from './type'
|
||||
|
||||
const props = defineProps(avatarProps)
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const { parent } = useInject<{ props: Required<AvatarGroupProps> }>(AVATAR_GROUP_KEY)
|
||||
|
||||
const show = ref(true)
|
||||
|
||||
const innerZIndex = ref<number | undefined>(undefined)
|
||||
|
||||
watch(() => ({
|
||||
maxCount: parent?.props.maxCount,
|
||||
children: parent?.internalChildren,
|
||||
}), ({ maxCount, children }) => {
|
||||
if (maxCount == null || Number(maxCount) <= 0 || children == null || instance == null) {
|
||||
show.value = true
|
||||
innerZIndex.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const index = children.findIndex((item) => {
|
||||
return item.uid === instance.uid && !(item.props.customClass as string)?.includes('avatar-fold')
|
||||
})
|
||||
|
||||
if (index < 0) {
|
||||
show.value = true
|
||||
innerZIndex.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
show.value = index < Number(maxCount)
|
||||
|
||||
if (parent?.props.zIndex === 'right')
|
||||
innerZIndex.value = children.length - index
|
||||
else
|
||||
innerZIndex.value = undefined
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
function getTrulySize() {
|
||||
if (props.size != null)
|
||||
return props.size
|
||||
|
||||
if (parent != null && parent.props.size != null)
|
||||
return parent.props.size
|
||||
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
const finalSize = computed<AvatarFinalSize>(() => {
|
||||
const size: string | number = getTrulySize()
|
||||
|
||||
const preset: boolean = avatarSize.includes(size as AvatarSize)
|
||||
|
||||
return {
|
||||
preset,
|
||||
value: preset ? (size as AvatarSize) : pxCheck(size),
|
||||
}
|
||||
})
|
||||
|
||||
const finalShape = computed<AvatarShape>(() => {
|
||||
if (props.shape != null)
|
||||
return props.shape
|
||||
|
||||
if (parent != null && parent.props.shape != null)
|
||||
return parent.props.shape
|
||||
|
||||
return 'round'
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
const value: Record<string, boolean> = {
|
||||
[`nut-avatar-${finalShape.value}`]: true,
|
||||
'nut-hidden': !show.value,
|
||||
}
|
||||
|
||||
if (finalSize.value.preset)
|
||||
value[`nut-avatar-${finalSize.value.value}`] = true
|
||||
|
||||
return getMainClass(props, componentName, value)
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
const value: CSSProperties = {
|
||||
backgroundColor: props.bgColor,
|
||||
color: props.customColor,
|
||||
}
|
||||
|
||||
if (!finalSize.value.preset) {
|
||||
value.width = finalSize.value.value
|
||||
value.height = finalSize.value.value
|
||||
}
|
||||
|
||||
if (parent?.props.span)
|
||||
value.marginLeft = pxCheck(parent?.props.span)
|
||||
|
||||
if (innerZIndex.value !== undefined)
|
||||
value.zIndex = innerZIndex.value
|
||||
|
||||
return getMainStyle(props, value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-avatar`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :style="styles" :class="classes">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
55
uni_modules/nutui-uni/components/avatar/index.scss
Normal file
55
uni_modules/nutui-uni/components/avatar/index.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
.nut-avatar {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex: 0 0 auto; // 防止被压缩
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 100% 100%;
|
||||
|
||||
image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nut-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-size: 100% 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.nut-avatar-large {
|
||||
width: $avatar-large-width;
|
||||
height: $avatar-large-height;
|
||||
line-height: $avatar-large-height;
|
||||
}
|
||||
|
||||
.nut-avatar-normal {
|
||||
width: $avatar-normal-width;
|
||||
height: $avatar-normal-height;
|
||||
line-height: $avatar-normal-height;
|
||||
}
|
||||
|
||||
.nut-avatar-small {
|
||||
width: $avatar-small-width;
|
||||
height: $avatar-small-height;
|
||||
line-height: $avatar-small-height;
|
||||
}
|
||||
|
||||
.nut-avatar-square {
|
||||
border-radius: $avatar-square;
|
||||
}
|
||||
|
||||
.nut-avatar-round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nut-avatar-square,
|
||||
.nut-avatar-round {
|
||||
overflow: hidden;
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/avatar/index.ts
Normal file
2
uni_modules/nutui-uni/components/avatar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './avatar'
|
||||
export * from './type'
|
||||
16
uni_modules/nutui-uni/components/avatar/type.ts
Normal file
16
uni_modules/nutui-uni/components/avatar/type.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const avatarSize = ['large', 'normal', 'small'] as const
|
||||
export type AvatarSize = (typeof avatarSize)[number]
|
||||
|
||||
export const avatarShape = ['round', 'square'] as const
|
||||
export type AvatarShape = (typeof avatarShape)[number]
|
||||
|
||||
export interface AvatarFinalSize {
|
||||
/**
|
||||
* 是否为预设尺寸
|
||||
*/
|
||||
preset: boolean
|
||||
/**
|
||||
* 尺寸值
|
||||
*/
|
||||
value: string
|
||||
}
|
||||
50
uni_modules/nutui-uni/components/avatargroup/avatargroup.ts
Normal file
50
uni_modules/nutui-uni/components/avatargroup/avatargroup.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, makeNumericProp, makeStringProp } from '../_utils'
|
||||
import type { AvatarShape, AvatarSize } from '../avatar'
|
||||
|
||||
export const AVATAR_GROUP_KEY = Symbol('avatarGroup')
|
||||
|
||||
export const avatargroupProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 显示的最大头像个数
|
||||
*/
|
||||
maxCount: makeNumericProp(-1),
|
||||
|
||||
/**
|
||||
* @description 头像数量超出时,会出现一个头像折叠元素,该元素内容可为`...`、`more`、`+N`
|
||||
*/
|
||||
maxContent: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 头像的大小,可选值为:`large`、`normal`、`small`,支持直接输入数字
|
||||
*/
|
||||
size: makeNumericProp<AvatarSize | string | number>('normal'),
|
||||
|
||||
/**
|
||||
* @description 头像的形状,可选值为:`square`、`round`
|
||||
*/
|
||||
shape: makeStringProp<AvatarShape>('round'),
|
||||
|
||||
/**
|
||||
* @description 头像折叠元素的字体颜色
|
||||
*/
|
||||
maxColor: makeStringProp('#666'),
|
||||
|
||||
/**
|
||||
* @description 头像折叠元素的背景色
|
||||
*/
|
||||
maxBgColor: makeStringProp('#eee'),
|
||||
|
||||
/**
|
||||
* @description 头像之间的间距
|
||||
*/
|
||||
span: makeNumericProp('-8'),
|
||||
|
||||
/**
|
||||
* @description 组合头像之间的层级方向,可选值为:`left`、`right`
|
||||
*/
|
||||
zIndex: makeStringProp<'left' | 'right'>('left'),
|
||||
}
|
||||
|
||||
export type AvatarGroupProps = ExtractPropTypes<typeof avatargroupProps>
|
||||
82
uni_modules/nutui-uni/components/avatargroup/avatargroup.vue
Normal file
82
uni_modules/nutui-uni/components/avatargroup/avatargroup.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, defineComponent, ref, watch } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { useProvide } from '../_hooks'
|
||||
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
|
||||
import NutAvatar from '../avatar/avatar.vue'
|
||||
import { AVATAR_GROUP_KEY, avatargroupProps } from './avatargroup'
|
||||
|
||||
const props = defineProps(avatargroupProps)
|
||||
|
||||
const { internalChildren } = useProvide(AVATAR_GROUP_KEY, `${PREFIX}-avatar`)({ props })
|
||||
|
||||
const innerMaxCount = computed<number>(() => {
|
||||
return Number(props.maxCount)
|
||||
})
|
||||
|
||||
const foldCount = ref(0)
|
||||
|
||||
watch(() => ({
|
||||
maxCount: props.maxCount,
|
||||
children: internalChildren,
|
||||
}), ({ children }) => {
|
||||
if (innerMaxCount.value > 0)
|
||||
foldCount.value = Math.min(99, children.length - innerMaxCount.value)
|
||||
else
|
||||
foldCount.value = 0
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
return getMainStyle(props, {
|
||||
marginLeft: `calc(0px - ${pxCheck(props.span)})`,
|
||||
})
|
||||
})
|
||||
|
||||
const foldStyles = computed<CSSProperties>(() => {
|
||||
return {
|
||||
marginLeft: pxCheck(props.span),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-avatar-group`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="styles">
|
||||
<slot />
|
||||
<NutAvatar
|
||||
v-if="foldCount > 0"
|
||||
custom-class="avatar-fold"
|
||||
:custom-style="foldStyles"
|
||||
:size="props.size"
|
||||
:shape="props.shape"
|
||||
:bg-color="props.maxBgColor"
|
||||
:custom-color="props.maxColor"
|
||||
>
|
||||
{{ props.maxContent || foldCount }}
|
||||
</NutAvatar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
12
uni_modules/nutui-uni/components/avatargroup/index.scss
Normal file
12
uni_modules/nutui-uni/components/avatargroup/index.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.nut-avatar-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 0 0 auto; // 防止被压缩
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 100% 100%;
|
||||
|
||||
.nut-avatar {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/avatargroup/index.ts
Normal file
1
uni_modules/nutui-uni/components/avatargroup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './avatargroup'
|
||||
44
uni_modules/nutui-uni/components/backtop/backtop.ts
Normal file
44
uni_modules/nutui-uni/components/backtop/backtop.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CLICK_EVENT } from '../_constants'
|
||||
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
|
||||
|
||||
export const backtopProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 滚动区域的高度
|
||||
*/
|
||||
height: makeStringProp('100vh'),
|
||||
|
||||
/**
|
||||
* @description 距离页面底部距离
|
||||
*/
|
||||
bottom: makeNumberProp(20),
|
||||
|
||||
/**
|
||||
* @description 距离页面右侧距离
|
||||
*/
|
||||
right: makeNumberProp(10),
|
||||
|
||||
/**
|
||||
* @description 页面垂直滚动多高后出现
|
||||
*/
|
||||
distance: makeNumberProp(200),
|
||||
|
||||
/**
|
||||
* @description 设置组件页面层级
|
||||
*/
|
||||
zIndex: makeNumberProp(10),
|
||||
|
||||
/**
|
||||
* @description 自定义图标颜色
|
||||
*/
|
||||
customColor: String,
|
||||
}
|
||||
|
||||
export type BacktopProps = ExtractPropTypes<typeof backtopProps>
|
||||
|
||||
export const backtopEmits = {
|
||||
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
|
||||
}
|
||||
|
||||
export type BacktopEmits = typeof backtopEmits
|
||||
78
uni_modules/nutui-uni/components/backtop/backtop.vue
Normal file
78
uni_modules/nutui-uni/components/backtop/backtop.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import { CLICK_EVENT, PREFIX } from '../_constants'
|
||||
import { getMainClass, getMainStyle } from '../_utils'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import { backtopEmits, backtopProps } from './backtop'
|
||||
|
||||
const props = defineProps(backtopProps)
|
||||
|
||||
const emit = defineEmits(backtopEmits)
|
||||
|
||||
const backTop = ref(false)
|
||||
const scrollTop = ref(1)
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
show: backTop.value,
|
||||
})
|
||||
})
|
||||
const style = computed(() => {
|
||||
return getMainStyle(props, {
|
||||
right: `${props.right}px`,
|
||||
bottom: `${props.bottom}px`,
|
||||
zIndex: props.zIndex,
|
||||
})
|
||||
})
|
||||
|
||||
function scroll(e: ScrollViewOnScrollEvent) {
|
||||
scrollTop.value = 2
|
||||
backTop.value = e.detail.scrollTop >= props.distance
|
||||
}
|
||||
|
||||
function click(e: unknown) {
|
||||
scrollTop.value = 1
|
||||
emit(CLICK_EVENT, e as MouseEvent)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-backtop`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<scroll-view
|
||||
:scroll-y="true"
|
||||
:style="{ height }"
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-with-animation="true"
|
||||
@scroll="scroll"
|
||||
>
|
||||
<slot name="content" />
|
||||
</scroll-view>
|
||||
<view :class="classes" :style="style" @click.stop="click">
|
||||
<slot name="icon">
|
||||
<NutIcon
|
||||
:custom-color="customColor"
|
||||
name="top"
|
||||
:size="19"
|
||||
custom-class="nut-backtop-main"
|
||||
/>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
40
uni_modules/nutui-uni/components/backtop/index.scss
Normal file
40
uni_modules/nutui-uni/components/backtop/index.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
.nut-theme-dark {
|
||||
.nut-backtop {
|
||||
&.show {
|
||||
color: $dark-color;
|
||||
background: $dark-background;
|
||||
border: 1px solid $dark-background;
|
||||
}
|
||||
|
||||
&-main {
|
||||
color:'#ffffff';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-backtop {
|
||||
position: fixed;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: $white;
|
||||
border: 1px solid $backtop-border-color;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.show :active {
|
||||
background: rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
&-main {
|
||||
color:'#000000';
|
||||
transition: all 0.2s ease-in-out
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
uni_modules/nutui-uni/components/backtop/index.ts
Normal file
1
uni_modules/nutui-uni/components/backtop/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type * from './backtop'
|
||||
52
uni_modules/nutui-uni/components/badge/badge.ts
Normal file
52
uni_modules/nutui-uni/components/badge/badge.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
|
||||
|
||||
export const badgeProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 显示的内容
|
||||
*/
|
||||
value: [String, Number],
|
||||
/**
|
||||
* @description `value` 为数值时,最大值
|
||||
*/
|
||||
max: makeNumberProp(10000),
|
||||
|
||||
/**
|
||||
* @description 是否为小点
|
||||
*/
|
||||
dot: Boolean,
|
||||
|
||||
/**
|
||||
* @description 是否为气泡形状
|
||||
* @since >v4.0.0
|
||||
*/
|
||||
bubble: Boolean,
|
||||
|
||||
/**
|
||||
* @description 是否隐藏
|
||||
*/
|
||||
hidden: Boolean,
|
||||
|
||||
/**
|
||||
* @description 上下偏移量,支持单位设置,可设置为:`5px` 等
|
||||
*/
|
||||
top: makeStringProp('0'),
|
||||
|
||||
/**
|
||||
* @description 左右偏移量,支持单位设置,可设置为:`5px` 等
|
||||
*/
|
||||
right: makeStringProp('0'),
|
||||
|
||||
/**
|
||||
* @description 徽标的 `z-index` 值
|
||||
*/
|
||||
zIndex: makeNumberProp(10),
|
||||
|
||||
/**
|
||||
* @description 徽标背景颜色
|
||||
*/
|
||||
customColor: makeStringProp(''),
|
||||
}
|
||||
|
||||
export type BadgeProps = ExtractPropTypes<typeof badgeProps>
|
||||
65
uni_modules/nutui-uni/components/badge/badge.vue
Normal file
65
uni_modules/nutui-uni/components/badge/badge.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { getMainClass, getMainStyle, pxCheck } from '../_utils'
|
||||
import { badgeProps } from './badge'
|
||||
|
||||
const props = defineProps(badgeProps)
|
||||
|
||||
const getStyle = computed(() => {
|
||||
return getMainStyle(props, {
|
||||
top: pxCheck(props.top),
|
||||
right: pxCheck(props.right),
|
||||
zIndex: props.zIndex,
|
||||
background: props.customColor,
|
||||
})
|
||||
})
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const content = computed(() => {
|
||||
if (props.dot)
|
||||
return
|
||||
const value = props.value
|
||||
const max = props.max
|
||||
if (typeof value === 'number' && typeof max === 'number')
|
||||
return max < value ? `${max}+` : value
|
||||
|
||||
return value
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-badge`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes">
|
||||
<view v-if="!props.hidden && !props.dot && $slots.icon" class="nut-badge__icon" :style="getStyle">
|
||||
<slot name="icon" />
|
||||
</view>
|
||||
<slot />
|
||||
<view
|
||||
v-if="!props.hidden && (content || props.dot)"
|
||||
class="nut-badge__content nut-badge__content--sup"
|
||||
:class="{ 'nut-badge__content--dot': props.dot, 'nut-badge__content--bubble': !props.dot && props.bubble }"
|
||||
:style="getStyle"
|
||||
>
|
||||
{{ content }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
54
uni_modules/nutui-uni/components/badge/index.scss
Normal file
54
uni_modules/nutui-uni/components/badge/index.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
.nut-theme-dark {
|
||||
.nut-badge {
|
||||
&.show {
|
||||
color: $dark-color;
|
||||
background: $dark-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-badge {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.nut-badge__icon {
|
||||
position: absolute;
|
||||
z-index: $badge-z-index;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $badge-icon-padding;
|
||||
line-height: normal;
|
||||
text-align: center;
|
||||
background: $badge-background-color;
|
||||
border-radius: $badge-border-radius;
|
||||
transform: $badge-content-transform;
|
||||
}
|
||||
|
||||
.nut-badge__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform: $badge-content-transform;
|
||||
|
||||
&--sup {
|
||||
position: absolute;
|
||||
padding: $badge-padding;
|
||||
font-size: $badge-font-size;
|
||||
font-weight: normal;
|
||||
color: $badge-color;
|
||||
text-align: center;
|
||||
background: $badge-background-color;
|
||||
border-radius: $badge-border-radius;
|
||||
}
|
||||
|
||||
&--dot {
|
||||
width: $badge-dot-width;
|
||||
height: $badge-dot-height;
|
||||
padding: $badge-dot-padding;
|
||||
border-radius: $badge-dot-border-radius;
|
||||
}
|
||||
|
||||
&--bubble {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/badge/index.ts
Normal file
1
uni_modules/nutui-uni/components/badge/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './badge'
|
||||
41
uni_modules/nutui-uni/components/barrage/barrage.ts
Normal file
41
uni_modules/nutui-uni/components/barrage/barrage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, makeArrayProp, makeNumberProp, truthProp } from '../_utils'
|
||||
|
||||
export const barrageProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 弹幕列表数据
|
||||
*/
|
||||
danmu: makeArrayProp<string>([]),
|
||||
|
||||
/**
|
||||
* @description 可视区域内每个弹幕出现的时间间隔
|
||||
*/
|
||||
frequency: makeNumberProp<number>(500),
|
||||
|
||||
/**
|
||||
* @description 每个弹幕的滚动时间
|
||||
*/
|
||||
speeds: makeNumberProp<number>(5000),
|
||||
|
||||
/**
|
||||
* @description 弹幕行数,分几行展示
|
||||
*/
|
||||
rows: makeNumberProp<number>(3),
|
||||
|
||||
/**
|
||||
* @description 弹幕垂直距离
|
||||
*/
|
||||
top: makeNumberProp<number>(10),
|
||||
|
||||
/**
|
||||
* @description 是否循环播放
|
||||
*/
|
||||
loop: truthProp,
|
||||
}
|
||||
|
||||
export type BarrageProps = ExtractPropTypes<typeof barrageProps>
|
||||
|
||||
export interface BarrageInst {
|
||||
add: (word: string) => void
|
||||
}
|
||||
132
uni_modules/nutui-uni/components/barrage/barrage.vue
Normal file
132
uni_modules/nutui-uni/components/barrage/barrage.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComponentInternalInstance } from 'vue'
|
||||
import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref, useSlots, watch } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { useSelectorQuery } from '../_hooks'
|
||||
import { getMainClass } from '../_utils'
|
||||
import { barrageProps } from './barrage'
|
||||
|
||||
const props = defineProps(barrageProps)
|
||||
|
||||
defineExpose({ add })
|
||||
|
||||
const instance = getCurrentInstance() as ComponentInternalInstance
|
||||
const { getSelectorNodeInfo } = useSelectorQuery(instance)
|
||||
const classTime = new Date().getTime()
|
||||
|
||||
const slotDefault = !!useSlots().default
|
||||
|
||||
const timeId = ref(new Date().getTime())
|
||||
const danmuList = ref<any>(props.danmu)
|
||||
const rows = ref<number>(props.rows)
|
||||
const top = ref<number>(props.top)
|
||||
const speeds = props.speeds
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
[`nut-barrage--dmBody${timeId.value}`]: true,
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef H5
|
||||
if (slotDefault) {
|
||||
const list = document
|
||||
.getElementsByClassName(`nut-barrage__slotBody${classTime}`)[0]
|
||||
.getElementsByClassName('nut-barrage__item')
|
||||
const childrens = list?.[0]?.children || []
|
||||
danmuList.value = childrens
|
||||
}
|
||||
// #endif
|
||||
runStep()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.danmu,
|
||||
(newValue) => {
|
||||
danmuList.value = [...newValue]
|
||||
},
|
||||
)
|
||||
|
||||
function add(word: string) {
|
||||
danmuList.value = [...danmuList.value, word]
|
||||
runStep()
|
||||
}
|
||||
|
||||
function getNode(index: number) {
|
||||
setTimeout(async () => {
|
||||
let width = 100
|
||||
const dmBodyNodeInfo = await getSelectorNodeInfo(`.nut-barrage--dmBody${timeId.value}`)
|
||||
width = dmBodyNodeInfo?.width || 300
|
||||
const itemNodeInfo = await getSelectorNodeInfo(`.nut-barrage__item${index}`)
|
||||
|
||||
const height = itemNodeInfo?.height
|
||||
const nodeTop = `${(index % rows.value) * (height! + top.value) + 20}px`
|
||||
styleInfo(index, nodeTop, width)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function runStep() {
|
||||
danmuList.value.forEach((item: any, index: number) => {
|
||||
getNode(index)
|
||||
})
|
||||
}
|
||||
const styleList: any[] = reactive([])
|
||||
function styleInfo(index: number, nodeTop: string, width: number) {
|
||||
const timeIndex = index - rows.value > 0 ? index - rows.value : 0
|
||||
const list = styleList
|
||||
const time = list[timeIndex] ? Number(list[timeIndex]['--time']) : 0
|
||||
// distance.value = '-' + (speeds / 1000) * 200 + '%';
|
||||
|
||||
const obj = {
|
||||
'top': nodeTop,
|
||||
'--time': `${props.frequency * index + time}`,
|
||||
'animationDuration': `${speeds}ms`,
|
||||
'animationIterationCount': `${props.loop ? 'infinite' : 1}`,
|
||||
'animationDelay': `${props.frequency * index + time}ms`,
|
||||
'--move-distance': `-${width}px`,
|
||||
}
|
||||
if (slotDefault && danmuList.value[index]?.el) {
|
||||
const orginalSty = danmuList.value[index].el.style
|
||||
danmuList.value[index].el.style = Object.assign(orginalSty, obj)
|
||||
}
|
||||
else {
|
||||
styleList.push(obj)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-barrage`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="customStyle">
|
||||
<div>
|
||||
<div :class="[`nut-barrage__slotBody${classTime}`]">
|
||||
<view
|
||||
v-for="(item, index) of danmuList"
|
||||
:key="`danmu${index}`"
|
||||
class="nut-barrage__item move"
|
||||
:class="[`nut-barrage__item${index}`]"
|
||||
:style="styleList[index]"
|
||||
>
|
||||
{{ item.length > 8 ? `${item.substr(0, 8)}...` : item }}
|
||||
</view>
|
||||
</div>
|
||||
</div>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
60
uni_modules/nutui-uni/components/barrage/index.scss
Normal file
60
uni_modules/nutui-uni/components/barrage/index.scss
Normal file
@@ -0,0 +1,60 @@
|
||||
.nut-barrage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
--move-distance: '300%';
|
||||
|
||||
&__item {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: block;
|
||||
width: 100px;
|
||||
padding: 3px 25px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
white-space: pre;
|
||||
background: linear-gradient(to right, rgb(0 0 0 / 15%), rgb(0 0 0 / 0%));
|
||||
border-radius: 50px;
|
||||
transform: translateX(100%);
|
||||
|
||||
&.move {
|
||||
animation-name: moving;
|
||||
animation-play-state: running;
|
||||
animation-timing-function: linear;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes moving {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(var(--move-distance));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moving {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(var(--move-distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-barrage {
|
||||
.nut-barrage__item {
|
||||
color: $dark-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/barrage/index.ts
Normal file
1
uni_modules/nutui-uni/components/barrage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type * from './barrage'
|
||||
139
uni_modules/nutui-uni/components/button/button.ts
Normal file
139
uni_modules/nutui-uni/components/button/button.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { ButtonLang, ButtonOnAddgroupappEvent, ButtonOnAgreeprivacyauthorizationEvent, ButtonOnChooseaddressEvent, ButtonOnChooseavatarEvent, ButtonOnChooseinvoicetitleEvent, ButtonOnErrorEvent, ButtonOnGetphonenumberEvent, ButtonOnLaunchappEvent, ButtonOnLoginEvent, ButtonOnOpensettingEvent, ButtonOnSubscribeEvent, ButtonOpenType } from '@uni-helper/uni-app-types'
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
import { CLICK_EVENT } from '../_constants'
|
||||
import { commonProps, makeNumberProp, makeStringProp } from '../_utils'
|
||||
import type { ButtonFormType, ButtonShape, ButtonSize, ButtonType } from './type'
|
||||
|
||||
export const buttonProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 指定按钮按下去的样式类
|
||||
*/
|
||||
hoverClass: makeStringProp('button-hover'),
|
||||
/**
|
||||
* @description 按住后多久出现点击态,单位毫秒
|
||||
*/
|
||||
hoverStartTime: makeNumberProp(20),
|
||||
/**
|
||||
* @description 手指松开后点击态保留时间,单位毫秒
|
||||
*/
|
||||
hoverStayTime: makeNumberProp(70),
|
||||
/**
|
||||
* @description 按钮颜色,支持传入 `linear-gradient` 渐变色
|
||||
*/
|
||||
customColor: String,
|
||||
/**
|
||||
* @description 形状,可选值为 `square` `round`
|
||||
*/
|
||||
shape: makeStringProp<ButtonShape>('round'),
|
||||
/**
|
||||
* @description 是否为朴素按钮
|
||||
*/
|
||||
plain: Boolean,
|
||||
/**
|
||||
* @description 按钮 `loading` 状态
|
||||
*/
|
||||
loading: Boolean,
|
||||
/**
|
||||
* @description 是否禁用按钮
|
||||
*/
|
||||
disabled: Boolean,
|
||||
/**
|
||||
* @description 按钮类型,可选值为 `primary` `info` `warning` `danger` `success` `default`
|
||||
*/
|
||||
type: makeStringProp<ButtonType>('default'),
|
||||
/**
|
||||
* @description 表单类型,可选值 `button` `submit` `reset`
|
||||
*/
|
||||
formType: makeStringProp<ButtonFormType>('button'),
|
||||
/**
|
||||
* @description 尺寸,可选值为 `large` `small` `mini` `normal`
|
||||
*/
|
||||
size: makeStringProp<ButtonSize>('normal'),
|
||||
/**
|
||||
* @description 是否为块级元素
|
||||
*/
|
||||
block: Boolean,
|
||||
/**
|
||||
* @description 小程序开放能力
|
||||
*/
|
||||
openType: String as PropType<ButtonOpenType>,
|
||||
/**
|
||||
* @description 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
|
||||
*/
|
||||
lang: makeStringProp<ButtonLang>('en'),
|
||||
/**
|
||||
* @description 会话来源,openType="contact"时有效
|
||||
*/
|
||||
sessionFrom: String,
|
||||
/**
|
||||
* @description 会话内消息卡片标题,openType="contact"时有效
|
||||
*/
|
||||
sendMessageTitle: String,
|
||||
/**
|
||||
* @description 会话内消息卡片点击跳转小程序路径,openType="contact"时有效
|
||||
*/
|
||||
sendMessagePath: String,
|
||||
/**
|
||||
* @description 会话内消息卡片图片,openType="contact"时有效
|
||||
*/
|
||||
sendMessageImg: String,
|
||||
/**
|
||||
* @description 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效
|
||||
*/
|
||||
showMessageCard: Boolean,
|
||||
/**
|
||||
* @description 打开群资料卡时,传递的群号,openType="openGroupProfile"时有效
|
||||
*/
|
||||
groupId: String,
|
||||
/**
|
||||
* @description 打开频道页面时,传递的频道号 openType="openGuildProfile"时有效
|
||||
*/
|
||||
guildId: makeStringProp(''),
|
||||
/**
|
||||
* @description 打开公众号资料卡时,传递的号码 openType="openPublicProfile"时有效
|
||||
*/
|
||||
publicId: String,
|
||||
/**
|
||||
* @description 客服的抖音号,openType="im"时有效
|
||||
*/
|
||||
dataImId: String,
|
||||
/**
|
||||
* @description IM卡片类型,openType="im"时有效
|
||||
*/
|
||||
dataImType: String,
|
||||
/**
|
||||
* @description 商品的id,仅支持泛知识课程库和生活服务商品库中的商品,openType="im"时有效
|
||||
*/
|
||||
dataGoodsId: String,
|
||||
/**
|
||||
* @description 订单的id,仅支持交易2.0订单, openType="im"时有效
|
||||
*/
|
||||
dataOrderId: String,
|
||||
/**
|
||||
* @description 商品类型,“1”代表生活服务,“2”代表泛知识。openType="im"时有效
|
||||
*/
|
||||
dataBizLine: String,
|
||||
} as const
|
||||
|
||||
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
|
||||
|
||||
export const buttonEmits = {
|
||||
[CLICK_EVENT]: (evt: MouseEvent) => evt instanceof Object,
|
||||
getphonenumber: (evt: ButtonOnGetphonenumberEvent) => evt instanceof Object,
|
||||
getuserinfo: (evt: any) => evt instanceof Object,
|
||||
error: (evt: ButtonOnErrorEvent) => evt instanceof Object,
|
||||
opensetting: (evt: ButtonOnOpensettingEvent) => evt instanceof Object,
|
||||
launchapp: (evt: ButtonOnLaunchappEvent) => evt instanceof Object,
|
||||
contact: (evt: any) => evt instanceof Object,
|
||||
chooseavatar: (evt: ButtonOnChooseavatarEvent) => evt instanceof Object,
|
||||
agreeprivacyauthorization: (evt: ButtonOnAgreeprivacyauthorizationEvent) => evt instanceof Object,
|
||||
addgroupapp: (evt: ButtonOnAddgroupappEvent) => evt instanceof Object,
|
||||
chooseaddress: (evt: ButtonOnChooseaddressEvent) => evt instanceof Object,
|
||||
chooseinvoicetitle: (evt: ButtonOnChooseinvoicetitleEvent) => evt instanceof Object,
|
||||
subscribe: (evt: ButtonOnSubscribeEvent) => evt instanceof Object,
|
||||
login: (evt: ButtonOnLoginEvent) => evt instanceof Object,
|
||||
im: (evt: any) => evt instanceof Object,
|
||||
}
|
||||
|
||||
export type ButtonEmits = typeof buttonEmits
|
||||
121
uni_modules/nutui-uni/components/button/button.vue
Normal file
121
uni_modules/nutui-uni/components/button/button.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { CLICK_EVENT, PREFIX } from '../_constants'
|
||||
import { getMainClass, getMainStyle } from '../_utils'
|
||||
import Icon from '../icon/icon.vue'
|
||||
import { buttonEmits, buttonProps } from './button'
|
||||
|
||||
const props = defineProps(buttonProps)
|
||||
|
||||
const emit = defineEmits(buttonEmits)
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
[`${componentName}--${props.type}`]: !!props.type,
|
||||
[`${componentName}--${props.size}`]: !!props.size,
|
||||
[`${componentName}--${props.shape}`]: !!props.shape,
|
||||
[`${componentName}--plain`]: props.plain,
|
||||
[`${componentName}--block`]: props.block,
|
||||
[`${componentName}--disabled`]: props.disabled,
|
||||
[`${componentName}--loading`]: props.loading,
|
||||
[`${componentName}--hovercls`]: props.hoverClass !== 'button-hover',
|
||||
})
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
const value: CSSProperties = {}
|
||||
|
||||
if (props.customColor) {
|
||||
if (props.plain) {
|
||||
value.color = props.customColor
|
||||
value.background = '#fff'
|
||||
|
||||
if (!props.customColor.includes('gradient'))
|
||||
value.borderColor = props.customColor
|
||||
}
|
||||
else {
|
||||
value.color = '#fff'
|
||||
value.background = props.customColor
|
||||
}
|
||||
}
|
||||
|
||||
return getMainStyle(props, value)
|
||||
})
|
||||
|
||||
function handleClick(event: any) {
|
||||
if (props.disabled || props.loading)
|
||||
return
|
||||
|
||||
emit(CLICK_EVENT, event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-button`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
// #ifndef H5
|
||||
styleIsolation: 'shared',
|
||||
// #endif
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:class="classes"
|
||||
:style="styles"
|
||||
:form-type="props.formType === 'button' ? undefined : props.formType"
|
||||
:open-type="props.disabled || props.loading ? undefined : props.openType"
|
||||
:hover-class="props.hoverClass"
|
||||
:hover-start-time="props.hoverStartTime"
|
||||
:hover-stay-time="props.hoverStayTime"
|
||||
hover-stop-propagation
|
||||
:lang="props.lang"
|
||||
:session-from="props.sessionFrom"
|
||||
:send-message-title="props.sendMessageTitle"
|
||||
:send-message-path="props.sendMessagePath"
|
||||
:send-message-img="props.sendMessageImg"
|
||||
:show-message-card="props.showMessageCard"
|
||||
:group-id="props.groupId"
|
||||
:guild-id="props.guildId"
|
||||
:public-id="props.publicId"
|
||||
:data-im-id="props.dataImId"
|
||||
:data-im-type="props.dataImType"
|
||||
:data-goods-id="props.dataGoodsId"
|
||||
:data-order-id="props.dataOrderId"
|
||||
:data-biz-line="props.dataBizLine"
|
||||
@click="handleClick"
|
||||
@getphonenumber="emit('getphonenumber', $event)"
|
||||
@getuserinfo="emit('getuserinfo', $event)"
|
||||
@error="emit('error', $event)"
|
||||
@opensetting="emit('opensetting', $event)"
|
||||
@addgroupapp="emit('addgroupapp', $event)"
|
||||
@chooseaddress="emit('chooseaddress', $event)"
|
||||
@chooseavatar="emit('chooseavatar', $event)"
|
||||
@chooseinvoicetitle="emit('chooseinvoicetitle', $event)"
|
||||
@launchapp="emit('launchapp', $event)"
|
||||
@login="emit('login', $event)"
|
||||
@subscribe="emit('subscribe', $event)"
|
||||
@contact="emit('contact', $event)"
|
||||
@agreeprivacyauthorization="emit('agreeprivacyauthorization', $event)"
|
||||
@im="emit('im', $event)"
|
||||
>
|
||||
<view class="nut-button__wrap">
|
||||
<Icon v-if="loading" name="loading" class="nut-icon-loading" />
|
||||
<slot v-if="$slots.icon && !loading" name="icon" />
|
||||
<view v-if="$slots.default" :class="{ 'nut-button__text': $slots.icon || loading }">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
292
uni_modules/nutui-uni/components/button/index.scss
Normal file
292
uni_modules/nutui-uni/components/button/index.scss
Normal file
@@ -0,0 +1,292 @@
|
||||
.nut-theme-dark {
|
||||
.nut-button {
|
||||
&--default {
|
||||
color: $dark-color3;
|
||||
background: $dark-background2;
|
||||
border: $button-border-width solid $dark-background2;
|
||||
}
|
||||
|
||||
&--plain {
|
||||
background: $dark-background2;
|
||||
}
|
||||
|
||||
&:not(.nut-button--hovercls) {
|
||||
.nut-button--plain:not([disabled]):active {
|
||||
background: $dark-background2;
|
||||
}
|
||||
|
||||
.nut-button--default:not([disabled]):active {
|
||||
color: $dark-color3;
|
||||
background: $dark-background2;
|
||||
border: $button-border-width solid $dark-background2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-button {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
height: $button-default-height;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: $button-default-font-size;
|
||||
line-height: $button-default-line-height;
|
||||
text-align: center;
|
||||
vertical-align: bottom;
|
||||
appearance: none;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.2s;
|
||||
-webkit-tap-highlight-color: rgb(0 0 0 / 0%);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
.nut-button__text {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background-color: $black;
|
||||
border: inherit;
|
||||
border-color: $black;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not(.nut-button--hovercls) {
|
||||
&:active::before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--loading,
|
||||
&--disabled {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--default {
|
||||
color: $button-default-color;
|
||||
background: $button-default-bg-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid $button-default-border-color;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
color: $button-primary-color;
|
||||
background: $button-primary-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
&--info {
|
||||
color: $button-info-color;
|
||||
background: $button-info-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: $button-success-color;
|
||||
background: $button-success-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
color: $button-danger-color;
|
||||
background: $button-danger-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: $button-warning-color;
|
||||
background: $button-warning-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
&--plain {
|
||||
background: $button-plain-background-color;
|
||||
background-origin: border-box;
|
||||
|
||||
&.nut-button--primary {
|
||||
color: $button-primary-border-color;
|
||||
border-color: $button-primary-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--info {
|
||||
color: $button-info-border-color;
|
||||
border-color: $button-info-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--success {
|
||||
color: $button-success-border-color;
|
||||
border-color: $button-success-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--danger {
|
||||
color: $button-danger-border-color;
|
||||
border-color: $button-danger-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--warning {
|
||||
color: $button-warning-border-color;
|
||||
border-color: $button-warning-border-color;
|
||||
}
|
||||
|
||||
&:not(.nut-button--hovercls) {
|
||||
&.nut-button--primary:not([disabled]):active {
|
||||
color: $button-primary-border-color;
|
||||
border-color: $button-primary-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--info:not([disabled]):active {
|
||||
color: $button-info-border-color;
|
||||
border-color: $button-info-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--success:not([disabled]):active {
|
||||
color: $button-success-border-color;
|
||||
border-color: $button-success-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--danger:not([disabled]):active {
|
||||
color: $button-danger-border-color;
|
||||
border-color: $button-danger-border-color;
|
||||
}
|
||||
|
||||
&.nut-button--warning:not([disabled]):active {
|
||||
color: $button-warning-border-color;
|
||||
border-color: $button-warning-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: 100%;
|
||||
height: $button-large-height;
|
||||
font-size: $button-large-font-size;
|
||||
line-height: $button-large-line-height;
|
||||
}
|
||||
|
||||
&--normal {
|
||||
padding: $button-default-padding;
|
||||
font-size: $button-default-font-size;
|
||||
}
|
||||
|
||||
&--small {
|
||||
height: $button-small-height;
|
||||
padding: $button-small-padding;
|
||||
font-size: $button-small-font-size;
|
||||
line-height: $button-small-line-height;
|
||||
|
||||
&.nut-button--round {
|
||||
border-radius: $button-small-round-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&--mini {
|
||||
height: $button-mini-height;
|
||||
padding: $button-mini-padding;
|
||||
font-size: $button-mini-font-size;
|
||||
line-height: $button-mini-line-height;
|
||||
}
|
||||
|
||||
&--block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: $button-disabled-opacity;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
cursor: default;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&--round {
|
||||
border-radius: $button-border-radius;
|
||||
}
|
||||
|
||||
&--square {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:not(.nut-button--hovercls) {
|
||||
.nut-button--default:not([disabled]):active {
|
||||
color: $button-default-color;
|
||||
background: $button-default-bg-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid $button-default-border-color;
|
||||
}
|
||||
|
||||
.nut-button--primary:not([disabled]):active {
|
||||
color: $button-primary-color;
|
||||
background: $button-primary-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
.nut-button--info:not([disabled]):active {
|
||||
color: $button-info-color;
|
||||
background: $button-info-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
.nut-button--success:not([disabled]):active {
|
||||
color: $button-success-color;
|
||||
background: $button-success-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
.nut-button--danger:not([disabled]):active {
|
||||
color: $button-danger-color;
|
||||
background: $button-danger-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
.nut-button--warning:not([disabled]):active {
|
||||
color: $button-warning-color;
|
||||
background: $button-warning-background-color;
|
||||
background-origin: border-box;
|
||||
border: $button-border-width solid transparent;
|
||||
}
|
||||
|
||||
.nut-button--plain:not([disabled]):active {
|
||||
background: $button-plain-background-color;
|
||||
background-origin: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
uni_modules/nutui-uni/components/button/index.ts
Normal file
2
uni_modules/nutui-uni/components/button/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './button'
|
||||
export * from './type'
|
||||
8
uni_modules/nutui-uni/components/button/type.ts
Normal file
8
uni_modules/nutui-uni/components/button/type.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const buttonType = ['default', 'primary', 'info', 'success', 'warning', 'danger'] as const
|
||||
export type ButtonType = (typeof buttonType)[number]
|
||||
export const buttonSize = ['large', 'normal', 'small', 'mini'] as const
|
||||
export type ButtonSize = (typeof buttonSize)[number]
|
||||
export const buttonShape = ['square', 'round'] as const
|
||||
export type ButtonShape = (typeof buttonShape)[number]
|
||||
export const buttonFormType = ['button', 'submit', 'reset'] as const
|
||||
export type ButtonFormType = (typeof buttonFormType)[number]
|
||||
125
uni_modules/nutui-uni/components/calendar/calendar.ts
Normal file
125
uni_modules/nutui-uni/components/calendar/calendar.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
|
||||
import {
|
||||
CHOOSE_EVENT,
|
||||
CLOSE_EVENT,
|
||||
CLOSED_EVENT,
|
||||
OPEN_EVENT,
|
||||
OPENED_EVENT,
|
||||
SELECT_EVENT,
|
||||
UPDATE_VISIBLE_EVENT,
|
||||
} from '../_constants'
|
||||
import { commonProps, getDay, makeNumberProp, makeStringProp, truthProp } from '../_utils'
|
||||
import { popupProps } from '../popup/popup'
|
||||
|
||||
export const calendarProps = {
|
||||
...popupProps,
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 是否可见
|
||||
*/
|
||||
visible: Boolean,
|
||||
/**
|
||||
* @description 类型,日期单选 `one`,区间选择 `range`,日期多选 `multiple`,周选择 `week`
|
||||
*/
|
||||
type: makeStringProp<'one' | 'range' | 'multiple' | 'week'>('one'),
|
||||
/**
|
||||
* @description 是否弹窗状态展示
|
||||
*/
|
||||
poppable: truthProp,
|
||||
/**
|
||||
* @description 自动回填
|
||||
*/
|
||||
isAutoBackFill: Boolean,
|
||||
/**
|
||||
* @description 显示标题
|
||||
*/
|
||||
title: makeStringProp('日期选择'),
|
||||
/**
|
||||
* @description 默认值,单个日期选择为 `string`,其他为 `string[]`
|
||||
*/
|
||||
defaultValue: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
},
|
||||
/**
|
||||
* @description 开始日期
|
||||
*/
|
||||
startDate: makeStringProp(getDay(0)),
|
||||
/**
|
||||
* @description 结束日期
|
||||
*/
|
||||
endDate: makeStringProp(getDay(365)),
|
||||
/**
|
||||
* @description 范围选择,开始信息文案
|
||||
*/
|
||||
startText: makeStringProp('开始'),
|
||||
/**
|
||||
* @description 范围选择,结束信息文案
|
||||
*/
|
||||
endText: makeStringProp('结束'),
|
||||
/**
|
||||
* @description 底部确认按钮文案
|
||||
*/
|
||||
confirmText: makeStringProp('确认'),
|
||||
/**
|
||||
* @description 是否展示今天标记
|
||||
*/
|
||||
showToday: truthProp,
|
||||
/**
|
||||
* @description 是否在展示日历标题
|
||||
*/
|
||||
showTitle: truthProp,
|
||||
/**
|
||||
* @description 是否展示日期标题
|
||||
*/
|
||||
showSubTitle: truthProp,
|
||||
/**
|
||||
* @description 是否启动滚动动画
|
||||
*/
|
||||
toDateAnimation: truthProp,
|
||||
/**
|
||||
* @description 设置周起始日
|
||||
*/
|
||||
firstDayOfWeek: makeNumberProp(0),
|
||||
/**
|
||||
* @description 一个用来判断该日期是否被禁用的函数,接受一个 `年 - 月 - 日` 作为参数。 应该返回一个 Boolean 值。
|
||||
* @default undefined
|
||||
*/
|
||||
disabledDate: Function,
|
||||
/**
|
||||
* @description 是否使用 footer 插槽,如果使用,此值必须为 true
|
||||
*/
|
||||
footerSlot: Boolean,
|
||||
/**
|
||||
* @description 是否使用 btn 插槽,如果使用,此值必须为 true
|
||||
*/
|
||||
btnSlot: Boolean,
|
||||
/**
|
||||
* @description 自定义弹窗样式
|
||||
*/
|
||||
popStyle: {
|
||||
type: [String, Object, Array] as PropType<StyleValue>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description 遮罩显示时的背景是否锁定
|
||||
*/
|
||||
lockScroll: truthProp,
|
||||
}
|
||||
|
||||
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
|
||||
|
||||
/* eslint-disable unused-imports/no-unused-vars */
|
||||
export const calendarEmits = {
|
||||
[UPDATE_VISIBLE_EVENT]: (value: boolean) => true,
|
||||
[CHOOSE_EVENT]: (value: string | object) => true,
|
||||
[SELECT_EVENT]: (value: any) => true,
|
||||
clickCloseIcon: () => true,
|
||||
clickOverlay: () => true,
|
||||
[OPEN_EVENT]: () => true,
|
||||
[OPENED_EVENT]: () => true,
|
||||
[CLOSE_EVENT]: () => true,
|
||||
[CLOSED_EVENT]: () => true,
|
||||
}
|
||||
/* eslint-enable unused-imports/no-unused-vars */
|
||||
|
||||
export type CalendarEmits = typeof calendarEmits
|
||||
262
uni_modules/nutui-uni/components/calendar/calendar.vue
Normal file
262
uni_modules/nutui-uni/components/calendar/calendar.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, ref, useSlots } from 'vue'
|
||||
import {
|
||||
CHOOSE_EVENT,
|
||||
CLOSE_EVENT,
|
||||
CLOSED_EVENT,
|
||||
OPEN_EVENT,
|
||||
OPENED_EVENT,
|
||||
PREFIX,
|
||||
SELECT_EVENT,
|
||||
UPDATE_VISIBLE_EVENT,
|
||||
} from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import NutCalendarItem from '../calendaritem/calendaritem.vue'
|
||||
import type { CalendarInst } from '../calendaritem/types'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import { calendarEmits, calendarProps } from './calendar'
|
||||
|
||||
const props = defineProps(calendarProps)
|
||||
|
||||
const emit = defineEmits(calendarEmits)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const innerVisible = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(value) {
|
||||
emit('update:visible', value)
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const popClasses = computed(() => {
|
||||
return `${componentName}__popup ${props.popClass}`
|
||||
})
|
||||
|
||||
const popStyles = computed(() => {
|
||||
return [{
|
||||
height: '85vh',
|
||||
}, props.popStyle]
|
||||
})
|
||||
|
||||
const overlayClasses = computed(() => {
|
||||
return `${componentName}__overlay ${props.overlayClass}`
|
||||
})
|
||||
|
||||
const calendarRef = ref<CalendarInst | null>(null)
|
||||
|
||||
function scrollToDate(date: string) {
|
||||
calendarRef.value?.scrollToDate(date)
|
||||
}
|
||||
|
||||
function initPosition() {
|
||||
calendarRef.value?.initPosition()
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
emit(CLOSE_EVENT)
|
||||
}
|
||||
|
||||
function choose(param: string | object) {
|
||||
close()
|
||||
|
||||
emit(CHOOSE_EVENT, param)
|
||||
}
|
||||
|
||||
function select(param: string) {
|
||||
emit(SELECT_EVENT, param)
|
||||
}
|
||||
|
||||
function update() {
|
||||
emit(UPDATE_VISIBLE_EVENT, false)
|
||||
}
|
||||
|
||||
function handleCloseIconClick() {
|
||||
emit('clickCloseIcon')
|
||||
}
|
||||
|
||||
function handleOverlayClick() {
|
||||
emit('clickOverlay')
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
emit(OPEN_EVENT)
|
||||
}
|
||||
|
||||
function handleOpened() {
|
||||
emit(OPENED_EVENT)
|
||||
|
||||
if (props.defaultValue) {
|
||||
if (Array.isArray(props.defaultValue)) {
|
||||
if (props.defaultValue.length > 0) {
|
||||
scrollToDate(props.defaultValue[0])
|
||||
}
|
||||
}
|
||||
else {
|
||||
scrollToDate(props.defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit(CLOSE_EVENT)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
emit(CLOSED_EVENT)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollToDate,
|
||||
initPosition,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-calendar`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="props.customStyle">
|
||||
<template v-if="props.poppable">
|
||||
<NutPopup
|
||||
v-model:visible="innerVisible"
|
||||
:custom-class="popClasses"
|
||||
:custom-style="popStyles"
|
||||
:overlay-class="overlayClasses"
|
||||
:overlay-style="props.overlayStyle"
|
||||
position="bottom"
|
||||
round
|
||||
:closeable="props.closeable"
|
||||
:close-icon="props.closeIcon"
|
||||
:close-icon-position="props.closeIconPosition"
|
||||
:z-index="props.zIndex"
|
||||
:lock-scroll="props.lockScroll"
|
||||
:overlay="props.overlay"
|
||||
:close-on-click-overlay="props.closeOnClickOverlay"
|
||||
:destroy-on-close="false"
|
||||
@click-close-icon="handleCloseIconClick"
|
||||
@click-overlay="handleOverlayClick"
|
||||
@open="handleOpen"
|
||||
@opened="handleOpened"
|
||||
@close="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<NutCalendarItem
|
||||
ref="calendarRef"
|
||||
:visible="innerVisible"
|
||||
:type="props.type"
|
||||
:poppable="props.poppable"
|
||||
:is-auto-back-fill="props.isAutoBackFill"
|
||||
:title="props.title"
|
||||
:default-value="props.defaultValue"
|
||||
:start-date="props.startDate"
|
||||
:end-date="props.endDate"
|
||||
:start-text="props.startText"
|
||||
:end-text="props.endText"
|
||||
:confirm-text="props.confirmText"
|
||||
:show-today="props.showToday"
|
||||
:show-title="props.showTitle"
|
||||
:show-sub-title="props.showSubTitle"
|
||||
:to-date-animation="props.toDateAnimation"
|
||||
:first-day-of-week="props.firstDayOfWeek"
|
||||
:disabled-date="props.disabledDate"
|
||||
:footer-slot="props.footerSlot"
|
||||
:btn-slot="props.btnSlot"
|
||||
@choose="choose"
|
||||
@select="select"
|
||||
@update="update"
|
||||
@close="close"
|
||||
>
|
||||
<template v-if="slots.btn" #btn>
|
||||
<slot name="btn" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.day" #day="{ date }">
|
||||
<slot name="day" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.topInfo" #topInfo="{ date }">
|
||||
<slot name="topInfo" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.bottomInfo" #bottomInfo="{ date }">
|
||||
<slot name="bottomInfo" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.footer" #footer="{ date }">
|
||||
<slot name="footer" :date="date" />
|
||||
</template>
|
||||
</NutCalendarItem>
|
||||
</NutPopup>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<NutCalendarItem
|
||||
ref="calendarRef"
|
||||
:visible="innerVisible"
|
||||
:type="props.type"
|
||||
:poppable="props.poppable"
|
||||
:is-auto-back-fill="props.isAutoBackFill"
|
||||
:title="props.title"
|
||||
:default-value="props.defaultValue"
|
||||
:start-date="props.startDate"
|
||||
:end-date="props.endDate"
|
||||
:start-text="props.startText"
|
||||
:end-text="props.endText"
|
||||
:confirm-text="props.confirmText"
|
||||
:show-today="props.showToday"
|
||||
:show-title="props.showTitle"
|
||||
:show-sub-title="props.showSubTitle"
|
||||
:to-date-animation="props.toDateAnimation"
|
||||
:first-day-of-week="props.firstDayOfWeek"
|
||||
:disabled-date="props.disabledDate"
|
||||
:footer-slot="props.footerSlot"
|
||||
:btn-slot="props.btnSlot"
|
||||
@choose="choose"
|
||||
@select="select"
|
||||
@close="close"
|
||||
>
|
||||
<template v-if="slots.btn" #btn>
|
||||
<slot name="btn" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.day" #day="{ date }">
|
||||
<slot name="day" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.topInfo" #topInfo="{ date }">
|
||||
<slot name="topInfo" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.bottomInfo" #bottomInfo="{ date }">
|
||||
<slot name="bottomInfo" :date="date" />
|
||||
</template>
|
||||
|
||||
<template v-if="slots.footer" #footer="{ date }">
|
||||
<slot name="footer" :date="date" />
|
||||
</template>
|
||||
</NutCalendarItem>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./index";
|
||||
</style>
|
||||
1
uni_modules/nutui-uni/components/calendar/index.scss
Normal file
1
uni_modules/nutui-uni/components/calendar/index.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import "../popup/index";
|
||||
1
uni_modules/nutui-uni/components/calendar/index.ts
Normal file
1
uni_modules/nutui-uni/components/calendar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './calendar'
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CHOOSE_EVENT, SELECT_EVENT } from '../_constants'
|
||||
import { commonProps, getDay, makeNumberProp, makeStringProp, truthProp } from '../_utils'
|
||||
|
||||
export const calendaritemProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 是否可见
|
||||
*/
|
||||
visible: Boolean,
|
||||
/**
|
||||
* @description 类型,日期单选 `one`,区间选择 `range`,日期多选 `multiple`,周选择 `week`
|
||||
*/
|
||||
type: makeStringProp<'one' | 'range' | 'multiple' | 'week'>('one'),
|
||||
/**
|
||||
* @description 是否弹窗状态展示
|
||||
*/
|
||||
poppable: truthProp,
|
||||
/**
|
||||
* @description 自动回填
|
||||
*/
|
||||
isAutoBackFill: Boolean,
|
||||
/**
|
||||
* @description 显示标题
|
||||
*/
|
||||
title: makeStringProp('日期选择'),
|
||||
/**
|
||||
* @description 默认值,单个日期选择为 `string`,其他为 `string[]`
|
||||
*/
|
||||
defaultValue: {
|
||||
type: [String, Array],
|
||||
},
|
||||
/**
|
||||
* @description 开始日期
|
||||
*/
|
||||
startDate: makeStringProp(getDay(0)),
|
||||
/**
|
||||
* @description 结束日期
|
||||
*/
|
||||
endDate: makeStringProp(getDay(365)),
|
||||
/**
|
||||
* @description 范围选择,开始信息文案
|
||||
*/
|
||||
startText: makeStringProp('开始'),
|
||||
/**
|
||||
* @description 范围选择,结束信息文案
|
||||
*/
|
||||
endText: makeStringProp('结束'),
|
||||
/**
|
||||
* @description 底部确认按钮文案
|
||||
*/
|
||||
confirmText: makeStringProp('确认'),
|
||||
/**
|
||||
* @description 是否展示今天标记
|
||||
*/
|
||||
showToday: truthProp,
|
||||
/**
|
||||
* @description 是否在展示日历标题
|
||||
*/
|
||||
showTitle: truthProp,
|
||||
/**
|
||||
* @description 是否展示日期标题
|
||||
*/
|
||||
showSubTitle: truthProp,
|
||||
/**
|
||||
* @description 是否启动滚动动画
|
||||
*/
|
||||
toDateAnimation: truthProp,
|
||||
/**
|
||||
* @description 设置周起始日
|
||||
*/
|
||||
firstDayOfWeek: makeNumberProp(0),
|
||||
/**
|
||||
* @description 一个用来判断该日期是否被禁用的函数,接受一个 `年 - 月 - 日` 作为参数。 应该返回一个 Boolean 值。
|
||||
* @default undefined
|
||||
*/
|
||||
disabledDate: Function,
|
||||
/**
|
||||
* @description 是否使用 footer 插槽,如果使用,此值必须为 true
|
||||
*/
|
||||
footerSlot: Boolean,
|
||||
/**
|
||||
* @description 是否使用 btn 插槽,如果使用,此值必须为 true
|
||||
*/
|
||||
btnSlot: Boolean,
|
||||
}
|
||||
|
||||
export type CalendarItemProps = ExtractPropTypes<typeof calendaritemProps>
|
||||
|
||||
/* eslint-disable unused-imports/no-unused-vars */
|
||||
export const calendaritemEmits = {
|
||||
[CHOOSE_EVENT]: (value: string | object) => true,
|
||||
[SELECT_EVENT]: (value: any) => true,
|
||||
update: () => true,
|
||||
close: () => true,
|
||||
}
|
||||
/* eslint-enable unused-imports/no-unused-vars */
|
||||
|
||||
export type CalendarItemEmits = typeof calendaritemEmits
|
||||
860
uni_modules/nutui-uni/components/calendaritem/calendaritem.vue
Normal file
860
uni_modules/nutui-uni/components/calendaritem/calendaritem.vue
Normal file
@@ -0,0 +1,860 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ScrollViewOnScrollEvent } from '@uni-helper/uni-app-types'
|
||||
import { computed, defineComponent, onMounted, reactive, ref, useSlots, watch } from 'vue'
|
||||
import { CHOOSE_EVENT, PREFIX, SELECT_EVENT } from '../_constants'
|
||||
import {
|
||||
compareDate,
|
||||
date2Str,
|
||||
formatResultDate,
|
||||
getDay,
|
||||
getMainClass,
|
||||
getMonthDays,
|
||||
getMonthPreDay,
|
||||
getMonthWeek,
|
||||
getNumTwoBit,
|
||||
getWeekDate,
|
||||
getWhatDay,
|
||||
getYearWeek,
|
||||
isEqual,
|
||||
isH5,
|
||||
} from '../_utils'
|
||||
import requestAniFrame from '../_utils/raf'
|
||||
import { useTranslate } from '../../locale'
|
||||
import { calendaritemEmits, calendaritemProps } from './calendaritem'
|
||||
import type { CalendarDateProp, CalendarTaroState, Day, MonthInfo, StringArr } from './types'
|
||||
|
||||
const props = defineProps(calendaritemProps)
|
||||
|
||||
const emit = defineEmits(calendaritemEmits)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const componentName = `${PREFIX}-calendar-item`
|
||||
|
||||
const { translate } = useTranslate(componentName)
|
||||
|
||||
const state: CalendarTaroState = reactive({
|
||||
yearMonthTitle: '',
|
||||
defaultRange: [],
|
||||
containerHeight: '100%',
|
||||
currDate: '',
|
||||
propStartDate: '',
|
||||
propEndDate: '',
|
||||
unLoadPrev: false,
|
||||
touchParams: {
|
||||
startY: 0,
|
||||
endY: 0,
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
lastY: 0,
|
||||
lastTime: 0,
|
||||
},
|
||||
transformY: 0,
|
||||
translateY: 0,
|
||||
scrollDistance: 0,
|
||||
defaultData: [],
|
||||
chooseData: [],
|
||||
monthsData: [],
|
||||
dayPrefix: 'nut-calendar__day',
|
||||
startData: '',
|
||||
endData: '',
|
||||
isRange: props.type === 'range',
|
||||
timer: 0,
|
||||
currentIndex: 0,
|
||||
avgHeight: 0,
|
||||
scrollTop: 0,
|
||||
monthsNum: 0,
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
'nut-calendar--nopop': !props.poppable,
|
||||
'nut-calendar--nofooter': props.isAutoBackFill,
|
||||
})
|
||||
})
|
||||
|
||||
// 新增:自定义周起始日
|
||||
const weekdays = (translate('weekdays') as any).map((day: string, index: number) => ({
|
||||
day,
|
||||
weekend: index === 0 || index === 6,
|
||||
}))
|
||||
|
||||
const weeks = ref([...weekdays.slice(props.firstDayOfWeek, 7), ...weekdays.slice(0, props.firstDayOfWeek)])
|
||||
|
||||
const months = ref<HTMLElement | null>(null)
|
||||
|
||||
const scalePx = ref(2)
|
||||
const viewHeight = ref(0)
|
||||
|
||||
const compConthsData = computed(() => {
|
||||
return state.monthsData.slice(state.defaultRange[0], state.defaultRange[1])
|
||||
})
|
||||
|
||||
const scrollWithAnimation = ref(false)
|
||||
|
||||
// 日期转化成数组
|
||||
function splitDate(date: string) {
|
||||
return date.split('-')
|
||||
}
|
||||
|
||||
// 判断是否为开始时间
|
||||
function isStart(currDate: string) {
|
||||
return isEqual(state.currDate[0], currDate)
|
||||
}
|
||||
|
||||
// 判断是否为结束时间
|
||||
function isEnd(currDate: string) {
|
||||
return isEqual(state.currDate[1], currDate)
|
||||
}
|
||||
|
||||
function isMultiple(currDate: string) {
|
||||
if (state.currDate?.length > 0) {
|
||||
return (state.currDate as StringArr)?.some((item: string) => {
|
||||
return isEqual(item, currDate)
|
||||
})
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前数据
|
||||
function getCurrDate(day: Day, month: MonthInfo) {
|
||||
return `${month.curData[0]}-${month.curData[1]}-${getNumTwoBit(+day.day)}`
|
||||
}
|
||||
|
||||
// 获取样式
|
||||
function getClass(day: Day, month: MonthInfo, index?: number) {
|
||||
const res = []
|
||||
if (
|
||||
typeof index === 'number'
|
||||
&& ((index + 1 + props.firstDayOfWeek) % 7 === 0 || (index + props.firstDayOfWeek) % 7 === 0)
|
||||
) {
|
||||
res.push('weekend')
|
||||
}
|
||||
|
||||
const currDate = getCurrDate(day, month)
|
||||
const { type } = props
|
||||
if (day.type === 'curr') {
|
||||
if (
|
||||
isEqual(state.currDate as string, currDate)
|
||||
|| ((type === 'range' || type === 'week') && (isStart(currDate) || isEnd(currDate)))
|
||||
|| (type === 'multiple' && isMultiple(currDate))
|
||||
) {
|
||||
res.push(`${state.dayPrefix}--active`)
|
||||
}
|
||||
else if (
|
||||
(state.propStartDate && compareDate(currDate, state.propStartDate))
|
||||
|| (state.propEndDate && compareDate(state.propEndDate, currDate))
|
||||
|| (props.disabledDate && props.disabledDate(currDate))
|
||||
) {
|
||||
res.push(`${state.dayPrefix}--disabled`)
|
||||
}
|
||||
else if (
|
||||
(type === 'range' || type === 'week')
|
||||
&& Array.isArray(state.currDate)
|
||||
&& Object.values(state.currDate).length === 2
|
||||
&& compareDate(state.currDate[0], currDate)
|
||||
&& compareDate(currDate, state.currDate[1])
|
||||
) {
|
||||
res.push(`${state.dayPrefix}--choose`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.push(`${state.dayPrefix}--disabled`)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 确认选择时触发
|
||||
function confirm() {
|
||||
const { type } = props
|
||||
if ((type === 'range' && state.chooseData.length === 2) || type !== 'range') {
|
||||
let selectData: any = state.chooseData.slice(0)
|
||||
if (type === 'week') {
|
||||
selectData = {
|
||||
weekDate: [handleWeekDate(state.chooseData[0] as string[]), handleWeekDate(state.chooseData[1] as string[])],
|
||||
}
|
||||
}
|
||||
emit(CHOOSE_EVENT, selectData)
|
||||
if (props.poppable)
|
||||
emit('update')
|
||||
}
|
||||
}
|
||||
|
||||
// 选中数据
|
||||
function chooseDay(day: Day, month: MonthInfo, isFirst = false) {
|
||||
if (!getClass(day, month).includes(`${state.dayPrefix}--disabled`)) {
|
||||
const { type } = props
|
||||
const [y, m] = month.curData
|
||||
const days = [...month.curData]
|
||||
days[2] = getNumTwoBit(Number(day.day))
|
||||
days[3] = `${days[0]}-${days[1]}-${days[2]}`
|
||||
days[4] = getWhatDay(+days[0], +days[1], +days[2])
|
||||
if (type === 'multiple') {
|
||||
if (state.currDate?.length > 0) {
|
||||
let hasIndex: number | undefined;
|
||||
(state.currDate as StringArr)?.forEach((item: string, index: number) => {
|
||||
if (item === days[3])
|
||||
hasIndex = index
|
||||
})
|
||||
if (isFirst) {
|
||||
state.chooseData.push([...days])
|
||||
}
|
||||
else {
|
||||
if (hasIndex !== undefined) {
|
||||
(state.currDate as StringArr).splice(hasIndex, 1)
|
||||
state.chooseData.splice(hasIndex, 1)
|
||||
}
|
||||
else {
|
||||
(state.currDate as StringArr).push(days[3])
|
||||
state.chooseData.push([...days])
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
state.currDate = [days[3]]
|
||||
state.chooseData = [[...days]]
|
||||
}
|
||||
}
|
||||
else if (type === 'range') {
|
||||
const curDataLength = Object.values(state.currDate).length
|
||||
if (curDataLength === 2 || curDataLength === 0) {
|
||||
state.currDate = [days[3]]
|
||||
}
|
||||
else {
|
||||
if (compareDate(state.currDate[0], days[3]))
|
||||
Array.isArray(state.currDate) && state.currDate.push(days[3])
|
||||
else
|
||||
Array.isArray(state.currDate) && state.currDate.unshift(days[3])
|
||||
}
|
||||
|
||||
if (state.chooseData.length === 2 || !state.chooseData.length) {
|
||||
state.chooseData = [[...days]]
|
||||
}
|
||||
else {
|
||||
if (compareDate(state.chooseData[0][3], days[3]))
|
||||
state.chooseData = [...state.chooseData, [...days]]
|
||||
else
|
||||
state.chooseData = [[...days], ...state.chooseData]
|
||||
}
|
||||
}
|
||||
else if (type === 'week') {
|
||||
const weekArr = getWeekDate(y, m, day.day, props.firstDayOfWeek)
|
||||
if (state.propStartDate && compareDate(weekArr[0], state.propStartDate))
|
||||
weekArr.splice(0, 1, state.propStartDate)
|
||||
|
||||
if (state.propEndDate && compareDate(state.propEndDate, weekArr[1]))
|
||||
weekArr.splice(1, 1, state.propEndDate)
|
||||
|
||||
state.currDate = weekArr
|
||||
state.chooseData = [formatResultDate(weekArr[0]), formatResultDate(weekArr[1])]
|
||||
}
|
||||
else {
|
||||
state.currDate = days[3]
|
||||
state.chooseData = [...days]
|
||||
}
|
||||
|
||||
if (!isFirst) {
|
||||
let selectData: any = state.chooseData
|
||||
if (type === 'week') {
|
||||
selectData = {
|
||||
weekDate: [
|
||||
handleWeekDate(state.chooseData[0] as string[]),
|
||||
handleWeekDate(state.chooseData[1] as string[]),
|
||||
],
|
||||
}
|
||||
}
|
||||
// 点击日期 触发
|
||||
emit(SELECT_EVENT, selectData)
|
||||
if (props.isAutoBackFill || !props.poppable)
|
||||
confirm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleWeekDate(weekDate: string[]) {
|
||||
const [y, m, d] = weekDate
|
||||
|
||||
return {
|
||||
date: weekDate,
|
||||
monthWeekNum: getMonthWeek(y, m, d, props.firstDayOfWeek),
|
||||
yearWeekNum: getYearWeek(y, m, d),
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前月数据
|
||||
function getCurrData(type: string) {
|
||||
const monthData = type === 'prev' ? state.monthsData[0] : state.monthsData[state.monthsData.length - 1]
|
||||
let year = Number.parseInt(monthData.curData[0])
|
||||
let month = Number.parseInt(monthData.curData[1].toString().replace(/^0/, ''))
|
||||
switch (type) {
|
||||
case 'prev':
|
||||
month === 1 && (year -= 1)
|
||||
month = month === 1 ? 12 : --month
|
||||
break
|
||||
case 'next':
|
||||
month === 12 && (year += 1)
|
||||
month = month === 12 ? 1 : ++month
|
||||
break
|
||||
}
|
||||
return [`${year}`, getNumTwoBit(month), `${getMonthDays(String(year), String(month))}`]
|
||||
}
|
||||
|
||||
// 获取日期状态
|
||||
function getDaysStatus(days: number, type: string, dateInfo: CalendarDateProp) {
|
||||
// 修复:当某个月的1号是周日时,月份下方会空出来一行
|
||||
const { year, month } = dateInfo
|
||||
if (type === 'prev' && days >= 7)
|
||||
days -= 7
|
||||
|
||||
return Array.from(Array.from({ length: days }), (v, k) => {
|
||||
return {
|
||||
day: String(k + 1),
|
||||
type,
|
||||
year,
|
||||
month,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取上一个月的最后一周天数,填充当月空白
|
||||
function getPreDaysStatus(days: number, type: string, dateInfo: CalendarDateProp, preCurrMonthDays: number) {
|
||||
// 新增:自定义周起始日
|
||||
days = days - props.firstDayOfWeek
|
||||
// 修复:当某个月的1号是周日时,月份下方会空出来一行
|
||||
const { year, month } = dateInfo
|
||||
if (type === 'prev' && days >= 7)
|
||||
days -= 7
|
||||
|
||||
const months = Array.from(Array.from({ length: preCurrMonthDays }), (v, k) => {
|
||||
return {
|
||||
day: String(k + 1),
|
||||
type,
|
||||
year,
|
||||
month,
|
||||
}
|
||||
})
|
||||
return months.slice(preCurrMonthDays - days)
|
||||
}
|
||||
|
||||
// 获取月数据
|
||||
function getMonth(curData: string[], type: string) {
|
||||
// 一号为周几
|
||||
const preMonthDays = getMonthPreDay(+curData[0], +curData[1])
|
||||
|
||||
let preMonth = Number(curData[1]) - 1
|
||||
let preYear = Number(curData[0])
|
||||
if (preMonth <= 0) {
|
||||
preMonth = 12
|
||||
preYear += 1
|
||||
}
|
||||
// 当月天数与上个月天数
|
||||
const currMonthDays = getMonthDays(String(curData[0]), String(curData[1]))
|
||||
const preCurrMonthDays = getMonthDays(`${preYear}`, `${preMonth}`)
|
||||
|
||||
const title = {
|
||||
year: curData[0],
|
||||
month: curData[1],
|
||||
}
|
||||
const monthInfo: MonthInfo = {
|
||||
curData,
|
||||
title: translate('monthTitle', title.year, title.month),
|
||||
monthData: [
|
||||
...(getPreDaysStatus(
|
||||
preMonthDays,
|
||||
'prev',
|
||||
{ month: String(preMonth), year: String(preYear) },
|
||||
preCurrMonthDays,
|
||||
) as Day[]),
|
||||
...(getDaysStatus(currMonthDays, 'curr', title) as Day[]),
|
||||
],
|
||||
cssHeight: 0,
|
||||
cssScrollHeight: 0,
|
||||
}
|
||||
let titleHeight, itemHeight
|
||||
if (isH5) {
|
||||
titleHeight = 46 * scalePx.value + 16 * scalePx.value * 2
|
||||
itemHeight = 128 * scalePx.value
|
||||
}
|
||||
else {
|
||||
titleHeight = Math.floor(46 * scalePx.value) + Math.floor(16 * scalePx.value) * 2
|
||||
itemHeight = Math.floor(128 * scalePx.value)
|
||||
}
|
||||
monthInfo.cssHeight = titleHeight + (monthInfo.monthData.length > 35 ? itemHeight * 6 : itemHeight * 5)
|
||||
|
||||
let cssScrollHeight = 0
|
||||
|
||||
if (state.monthsData.length > 0) {
|
||||
cssScrollHeight
|
||||
= (state.monthsData[state.monthsData.length - 1] as MonthInfo).cssScrollHeight
|
||||
+ (state.monthsData[state.monthsData.length - 1] as MonthInfo).cssHeight
|
||||
}
|
||||
monthInfo.cssScrollHeight = cssScrollHeight
|
||||
if (type === 'next') {
|
||||
// 判断当前日期 是否大于 最后一天
|
||||
if (
|
||||
!state.endData
|
||||
|| !compareDate(
|
||||
`${state.endData[0]}-${state.endData[1]}-${getMonthDays(state.endData[0], state.endData[1])}`,
|
||||
`${curData[0]}-${curData[1]}-${curData[2]}`,
|
||||
)
|
||||
) {
|
||||
state.monthsData.push(monthInfo)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 判断当前日期 是否小于 第一天
|
||||
if (
|
||||
!state.startData
|
||||
|| !compareDate(
|
||||
`${curData[0]}-${curData[1]}-${curData[2]}`,
|
||||
`${state.startData[0]}-${state.startData[1]}-01`,
|
||||
)
|
||||
) {
|
||||
state.monthsData.unshift(monthInfo)
|
||||
}
|
||||
else {
|
||||
state.unLoadPrev = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
function initData() {
|
||||
// 初始化开始结束数据
|
||||
const propStartDate = props.startDate ? props.startDate : getDay(0)
|
||||
const propEndDate = props.endDate ? props.endDate : getDay(365)
|
||||
state.propStartDate = propStartDate
|
||||
state.propEndDate = propEndDate
|
||||
state.startData = splitDate(propStartDate)
|
||||
state.endData = splitDate(propEndDate)
|
||||
|
||||
// 根据是否存在默认时间,初始化当前日期,
|
||||
if (props.defaultValue || (Array.isArray(props.defaultValue) && (props.defaultValue as any[]).length > 0)) {
|
||||
state.currDate
|
||||
= props.type !== 'one' ? ([...props.defaultValue] as StringArr) : (props.defaultValue as string | StringArr)
|
||||
}
|
||||
|
||||
// 判断时间范围内存在多少个月
|
||||
const startDate = {
|
||||
year: Number(state.startData[0]),
|
||||
month: Number(state.startData[1]),
|
||||
}
|
||||
const endDate = {
|
||||
year: Number(state.endData[0]),
|
||||
month: Number(state.endData[1]),
|
||||
}
|
||||
let monthsNum = endDate.month - startDate.month
|
||||
if (endDate.year - startDate.year > 0)
|
||||
monthsNum = monthsNum + 12 * (endDate.year - startDate.year)
|
||||
|
||||
if (monthsNum <= 0)
|
||||
monthsNum = 1
|
||||
|
||||
// 设置月份数据
|
||||
getMonth(state.startData, 'next')
|
||||
|
||||
let i = 1
|
||||
do
|
||||
getMonth(getCurrData('next'), 'next')
|
||||
while (i++ < monthsNum)
|
||||
state.monthsNum = monthsNum
|
||||
|
||||
// 日期转化为数组,限制初始日期。判断时间范围
|
||||
if (props.type === 'range' && Array.isArray(state.currDate)) {
|
||||
if (state.currDate.length > 0) {
|
||||
if (propStartDate && compareDate(state.currDate[0], propStartDate))
|
||||
state.currDate.splice(0, 1, propStartDate)
|
||||
|
||||
if (propEndDate && compareDate(propEndDate, state.currDate[1]))
|
||||
state.currDate.splice(1, 1, propEndDate)
|
||||
|
||||
state.defaultData = [...splitDate(state.currDate[0]), ...splitDate(state.currDate[1])]
|
||||
}
|
||||
}
|
||||
else if (props.type === 'multiple' && Array.isArray(state.currDate)) {
|
||||
if (state.currDate.length > 0) {
|
||||
const defaultArr: string[] = []
|
||||
const obj: any = {}
|
||||
state.currDate.forEach((item: string) => {
|
||||
if (
|
||||
propStartDate
|
||||
&& !compareDate(item, propStartDate)
|
||||
&& propEndDate
|
||||
&& !compareDate(propEndDate, item)
|
||||
) {
|
||||
if (!Object.hasOwnProperty.call(obj, item)) {
|
||||
defaultArr.push(item)
|
||||
obj[item] = item
|
||||
}
|
||||
}
|
||||
})
|
||||
state.currDate = [...defaultArr]
|
||||
state.defaultData = [...splitDate(defaultArr[0])]
|
||||
}
|
||||
}
|
||||
else if (props.type === 'week' && Array.isArray(state.currDate)) {
|
||||
if (state.currDate.length > 0) {
|
||||
const [y, m, d] = splitDate(state.currDate[0])
|
||||
|
||||
state.currDate = getWeekDate(y, m, d, props.firstDayOfWeek)
|
||||
|
||||
if (propStartDate && compareDate(state.currDate[0], propStartDate))
|
||||
state.currDate.splice(0, 1, propStartDate)
|
||||
|
||||
if (propEndDate && compareDate(propEndDate, state.currDate[1]))
|
||||
state.currDate.splice(1, 1, propEndDate)
|
||||
|
||||
state.defaultData = [...splitDate(state.currDate[0]), ...splitDate(state.currDate[1])]
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (state.currDate) {
|
||||
if (propStartDate && compareDate(state.currDate as string, propStartDate))
|
||||
state.currDate = propStartDate
|
||||
else if (propEndDate && !compareDate(state.currDate as string, propEndDate))
|
||||
state.currDate = propEndDate
|
||||
|
||||
state.defaultData = [...splitDate(state.currDate as string)]
|
||||
}
|
||||
}
|
||||
// 设置默认可见区域
|
||||
let current = 0
|
||||
let lastCurrent = 0
|
||||
if (state.defaultData.length > 0) {
|
||||
state.monthsData.forEach((item, index) => {
|
||||
if (item.title === translate('monthTitle', state.defaultData[0], state.defaultData[1]))
|
||||
current = index
|
||||
|
||||
if (props.type === 'range' || props.type === 'week') {
|
||||
if (item.title === translate('monthTitle', state.defaultData[3], state.defaultData[4]))
|
||||
lastCurrent = index
|
||||
}
|
||||
})
|
||||
}
|
||||
setDefaultRange(monthsNum, current)
|
||||
state.currentIndex = current
|
||||
state.yearMonthTitle = state.monthsData[state.currentIndex].title
|
||||
if (state.defaultData.length > 0) {
|
||||
// 设置当前选中日期
|
||||
if (state.isRange) {
|
||||
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
|
||||
chooseDay({ day: state.defaultData[5], type: 'curr' }, state.monthsData[lastCurrent], true)
|
||||
}
|
||||
else if (props.type === 'week') {
|
||||
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
|
||||
}
|
||||
else if (props.type === 'multiple') {
|
||||
[...state.currDate].forEach((item: string) => {
|
||||
const dateArr = splitDate(item)
|
||||
let current = state.currentIndex
|
||||
state.monthsData.forEach((item, index) => {
|
||||
if (item.title === translate('monthTitle', dateArr[0], dateArr[1]))
|
||||
current = index
|
||||
})
|
||||
chooseDay({ day: dateArr[2], type: 'curr' }, state.monthsData[current], true)
|
||||
})
|
||||
}
|
||||
else {
|
||||
chooseDay({ day: state.defaultData[2], type: 'curr' }, state.monthsData[state.currentIndex], true)
|
||||
}
|
||||
}
|
||||
|
||||
const lastItem = state.monthsData[state.monthsData.length - 1]
|
||||
const containerHeight = lastItem.cssHeight + lastItem.cssScrollHeight
|
||||
|
||||
state.containerHeight = `${containerHeight}px`
|
||||
state.scrollTop = Math.ceil(state.monthsData[state.currentIndex].cssScrollHeight)
|
||||
state.avgHeight = Math.floor(containerHeight / (monthsNum + 1))
|
||||
|
||||
if (months?.value)
|
||||
viewHeight.value = months.value.clientHeight
|
||||
}
|
||||
|
||||
function scrollToDate(date: string) {
|
||||
if (compareDate(date, state.propStartDate))
|
||||
date = state.propStartDate
|
||||
else if (!compareDate(date, state.propEndDate))
|
||||
date = state.propEndDate
|
||||
|
||||
const dateArr = splitDate(date)
|
||||
|
||||
state.monthsData.forEach((item, index) => {
|
||||
if (item.title === translate('monthTitle', dateArr[0], dateArr[1])) {
|
||||
// scrollTop 不会实时变更。当再次赋值时,scrollTop无变化时,不会触发滚动
|
||||
state.scrollTop += 1
|
||||
scrollWithAnimation.value = props.toDateAnimation
|
||||
requestAniFrame(() => {
|
||||
setTimeout(() => {
|
||||
state.scrollTop = state.monthsData[index].cssScrollHeight
|
||||
|
||||
setTimeout(() => {
|
||||
scrollWithAnimation.value = false
|
||||
}, 200)
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function initPosition() {
|
||||
state.scrollTop = Math.ceil(state.monthsData[state.currentIndex].cssScrollHeight)
|
||||
}
|
||||
|
||||
// 设置当前可见月份
|
||||
function setDefaultRange(monthsNum: number, current: number) {
|
||||
if (monthsNum >= 3) {
|
||||
if (current > 0 && current < monthsNum)
|
||||
state.defaultRange = [current - 1, current + 3]
|
||||
|
||||
else if (current === 0)
|
||||
state.defaultRange = [current, current + 4]
|
||||
|
||||
else if (current === monthsNum)
|
||||
state.defaultRange = [current - 2, current + 2]
|
||||
}
|
||||
else {
|
||||
state.defaultRange = [0, monthsNum + 2]
|
||||
}
|
||||
|
||||
state.translateY = state.monthsData[state.defaultRange[0]].cssScrollHeight
|
||||
}
|
||||
|
||||
// 区间选择&&当前月&&选中态
|
||||
function isActive(day: Day, month: MonthInfo) {
|
||||
return (
|
||||
(props.type === 'range' || props.type === 'week')
|
||||
&& day.type === 'curr'
|
||||
&& getClass(day, month).includes('nut-calendar__day--active')
|
||||
)
|
||||
}
|
||||
|
||||
// 是否有开始提示
|
||||
function isStartTip(day: Day, month: MonthInfo) {
|
||||
return isActive(day, month) && isStart(getCurrDate(day, month))
|
||||
}
|
||||
|
||||
// 是否有结束提示
|
||||
function isEndTip(day: Day, month: MonthInfo) {
|
||||
if (state.currDate.length >= 2 && isEnd(getCurrDate(day, month)))
|
||||
return isActive(day, month)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 开始结束时间是否相等
|
||||
function rangeTip() {
|
||||
if (state.currDate.length >= 2)
|
||||
return isEqual(state.currDate[0], state.currDate[1])
|
||||
}
|
||||
|
||||
// 是否有 当前日期
|
||||
function isCurrDay(dateInfo: Day) {
|
||||
const date = `${dateInfo.year}-${dateInfo.month}-${Number(dateInfo.day) < 10 ? `0${dateInfo.day}` : dateInfo.day}`
|
||||
return isEqual(date, date2Str(new Date()))
|
||||
}
|
||||
|
||||
// 滚动处理事件
|
||||
function mothsViewScroll(e: ScrollViewOnScrollEvent) {
|
||||
if (state.monthsData.length <= 1)
|
||||
return
|
||||
|
||||
const currentScrollTop = e.detail.scrollTop
|
||||
let current = Math.floor(currentScrollTop / state.avgHeight)
|
||||
if (current === 0) {
|
||||
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight)
|
||||
current += 1
|
||||
}
|
||||
else if (current > 0 && current < state.monthsNum - 1) {
|
||||
if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight)
|
||||
current += 1
|
||||
|
||||
if (currentScrollTop < state.monthsData[current].cssScrollHeight)
|
||||
current -= 1
|
||||
}
|
||||
if (state.currentIndex !== current) {
|
||||
state.currentIndex = current
|
||||
setDefaultRange(state.monthsNum, current)
|
||||
}
|
||||
state.yearMonthTitle = state.monthsData[current].title
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
function resetRender() {
|
||||
state.chooseData.splice(0)
|
||||
state.monthsData.splice(0)
|
||||
initData()
|
||||
}
|
||||
|
||||
// 监听 默认值更改
|
||||
watch(() => props.defaultValue, (value) => {
|
||||
if (value) {
|
||||
if (props.poppable) {
|
||||
resetRender()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化数据
|
||||
uni.getSystemInfo({
|
||||
success(res) {
|
||||
let scale = 2
|
||||
let toFixed = 3
|
||||
if (isH5) {
|
||||
toFixed = 5
|
||||
const fontSize = document.documentElement.style.fontSize
|
||||
scale = Number((Number.parseInt(fontSize) / 40).toFixed(toFixed))
|
||||
}
|
||||
else {
|
||||
const screenWidth = res.screenWidth
|
||||
scale = Number((screenWidth / 750).toFixed(toFixed))
|
||||
}
|
||||
scalePx.value = scale
|
||||
initData()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
scrollToDate,
|
||||
initPosition,
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
name: `${PREFIX}-calendar-item`,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="props.customStyle">
|
||||
<view class="nut-calendar__header">
|
||||
<view v-if="props.showTitle" class="nut-calendar__header-title">
|
||||
{{ props.title || translate('title') }}
|
||||
</view>
|
||||
|
||||
<view v-if="props.btnSlot" class="nut-calendar__header-slot">
|
||||
<slot name="btn" />
|
||||
</view>
|
||||
|
||||
<view v-if="props.showSubTitle" class="nut-calendar__header-subtitle">
|
||||
{{ state.yearMonthTitle }}
|
||||
</view>
|
||||
|
||||
<view class="nut-calendar__weekdays">
|
||||
<view
|
||||
v-for="(item, index) of weeks"
|
||||
:key="index"
|
||||
class="nut-calendar__weekday"
|
||||
:class="{ weekend: item.weekend }"
|
||||
>
|
||||
{{ item.day }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
ref="months"
|
||||
class="nut-calendar__content"
|
||||
:scroll-y="true"
|
||||
:scroll-top="state.scrollTop"
|
||||
:scroll-with-animation="scrollWithAnimation"
|
||||
@scroll="mothsViewScroll"
|
||||
>
|
||||
<view class="nut-calendar__panel" :style="{ height: state.containerHeight }">
|
||||
<view class="nut-calendar__body" :style="{ transform: `translateY(${state.translateY}px)` }">
|
||||
<view v-for="(month, index) of compConthsData" :key="index" class="nut-calendar__month">
|
||||
<view class="nut-calendar__month-title">
|
||||
{{ month.title }}
|
||||
</view>
|
||||
|
||||
<view class="nut-calendar__days">
|
||||
<view
|
||||
class="nut-calendar__days-item"
|
||||
:class="{ 'nut-calendar__days-item--range': props.type === 'range' }"
|
||||
>
|
||||
<template v-for="(day, i) of month.monthData" :key="i">
|
||||
<view
|
||||
class="nut-calendar__day"
|
||||
:class="getClass(day, month, i)"
|
||||
@click="chooseDay(day, month)"
|
||||
>
|
||||
<!-- 日期显示slot -->
|
||||
<view class="nut-calendar__day-value">
|
||||
<!-- #ifdef MP -->
|
||||
{{ day.type === 'curr' ? day.day : '' }}
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP -->
|
||||
<slot name="day" :date="day.type === 'curr' ? day : ''">
|
||||
{{ day.type === 'curr' ? day.day : '' }}
|
||||
</slot>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="slots.topInfo" class="nut-calendar__day-tips nut-calendar__day-tips--top">
|
||||
<slot name="topInfo" :date="day.type === 'curr' ? day : ''" />
|
||||
</view>
|
||||
|
||||
<view v-if="slots.bottomInfo" class="nut-calendar__day-tips nut-calendar__day-tips--bottom">
|
||||
<slot name="bottomInfo" :date="day.type === 'curr' ? day : ''" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP -->
|
||||
<view
|
||||
v-if="!slots.bottomInfo && props.showToday && isCurrDay(day)"
|
||||
class="nut-calendar__day-tips--curr"
|
||||
>
|
||||
{{ translate('today') }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef MP -->
|
||||
<view v-if="props.showToday && isCurrDay(day)" class="nut-calendar__day-tips--curr">
|
||||
{{ translate('today') }}
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<view
|
||||
v-if="isStartTip(day, month)"
|
||||
class="nut-calendar__day-tip"
|
||||
:class="{ 'nut-calendar__day-tips--top': rangeTip() }"
|
||||
>
|
||||
{{ props.startText || translate('start') }}
|
||||
</view>
|
||||
|
||||
<view v-if="isEndTip(day, month)" class="nut-calendar__day-tip">
|
||||
{{ props.endText || translate('end') }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="props.poppable && !props.isAutoBackFill" class="nut-calendar__footer">
|
||||
<slot v-if="props.footerSlot" name="footer" :date="state.chooseData" />
|
||||
|
||||
<view v-else class="nut-calendar__confirm" @click="confirm">
|
||||
{{ props.confirmText || translate('confirm') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./index";
|
||||
</style>
|
||||
270
uni_modules/nutui-uni/components/calendaritem/index.scss
Normal file
270
uni_modules/nutui-uni/components/calendaritem/index.scss
Normal file
@@ -0,0 +1,270 @@
|
||||
.nut-theme-dark {
|
||||
.nut-calendar {
|
||||
$block: &;
|
||||
|
||||
&-item {
|
||||
color: $dark-color;
|
||||
background: $dark-background;
|
||||
}
|
||||
|
||||
&__header {
|
||||
color: $dark-color;
|
||||
background: $dark-background;
|
||||
}
|
||||
|
||||
&__content {
|
||||
#{$block}__panel {
|
||||
#{$block}__days {
|
||||
#{$block}__day {
|
||||
&--disabled {
|
||||
color: $dark-calendar-disabled !important;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-month-day {
|
||||
&-choose {
|
||||
color: $calendar-choose-font-color;
|
||||
background-color: $dark-calendar-choose-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
color: $dark-color;
|
||||
background: $dark-background2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-calendar {
|
||||
$block: &;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&#{$block}--nopop {
|
||||
#{$block}__header {
|
||||
#{$block}__header-title {
|
||||
font-size: $calendar-base-font;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popup-box {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 1px;
|
||||
text-align: center;
|
||||
background-color: $white;
|
||||
|
||||
&-title {
|
||||
font-size: $calendar-title-font;
|
||||
font-weight: $calendar-title-font-weight;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
&-slot {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
padding: 7px 0;
|
||||
font-size: $calendar-sub-title-font;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
#{$block}__weekdays {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 36px;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 4px 10px 0 rgba($color: #000, $alpha: 6%);
|
||||
|
||||
#{$block}__weekday {
|
||||
&.weekend {
|
||||
color: $calendar-day67-font-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
#{$block}__panel {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
#{$block}__body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#{$block}__month {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
view:nth-of-type(2) {
|
||||
#{$block}__month-title {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-loading-tip {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
font-size: $calendar-text-font;
|
||||
line-height: 50px;
|
||||
color: $text-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#{$block}__month-title {
|
||||
height: 23px;
|
||||
margin: 8px 0;
|
||||
font-size: $calendar-month-title-font-size;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
#{$block}__days {
|
||||
overflow: hidden;
|
||||
|
||||
#{$block}__day {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
float: left;
|
||||
width: 14.28%;
|
||||
height: 64px;
|
||||
font-weight: $calendar-day-font-weight;
|
||||
|
||||
&.weekend {
|
||||
color: $calendar-day67-font-color;
|
||||
}
|
||||
|
||||
#{$block}__day-tips {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#{$block}__day-tips--curr {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
#{$block}__day-tip {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
color: $calendar-primary-color;
|
||||
}
|
||||
|
||||
#{$block}__day-tips--top {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
#{$block}__day-tips--bottom {
|
||||
bottom: 6px;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $white !important;
|
||||
background-color: $calendar-primary-color;
|
||||
border-radius: $calendar-day-active-border-radius;
|
||||
|
||||
#{$block}__day-tips {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#{$block}__day-tips--curr {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#{$block}__day-tip {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: $calendar-disable-color !important;
|
||||
}
|
||||
|
||||
&--choose {
|
||||
color: $calendar-choose-font-color;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
background-color: $calendar-choose-color;
|
||||
opacity: 0.09;
|
||||
}
|
||||
}
|
||||
|
||||
#{$block}__day-value {
|
||||
padding: 2px 0;
|
||||
font-size: $calendar-day-font;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
background-color: $white;
|
||||
|
||||
#{$block}__confirm {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
margin: 10px 18px;
|
||||
line-height: 44px;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
background: $button-primary-background-color;
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/calendaritem/index.ts
Normal file
1
uni_modules/nutui-uni/components/calendaritem/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './calendaritem'
|
||||
65
uni_modules/nutui-uni/components/calendaritem/types.ts
Normal file
65
uni_modules/nutui-uni/components/calendaritem/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export interface TouchParam {
|
||||
startY: number
|
||||
endY: number
|
||||
startTime: number
|
||||
endTime: number
|
||||
lastY: number
|
||||
lastTime: number
|
||||
}
|
||||
|
||||
export type InputDate = string | string[]
|
||||
export type StringArr = string[]
|
||||
|
||||
export interface CalendarState {
|
||||
yearMonthTitle: string
|
||||
currDate: string | string[]
|
||||
propStartDate: string
|
||||
propEndDate: string
|
||||
currentIndex: number
|
||||
unLoadPrev: boolean
|
||||
touchParams: TouchParam
|
||||
transformY: number
|
||||
translateY: number
|
||||
scrollDistance: number
|
||||
defaultData: InputDate
|
||||
chooseData: (string | string[])[]
|
||||
monthsData: MonthInfo[]
|
||||
dayPrefix: string
|
||||
startData: InputDate
|
||||
endData: InputDate
|
||||
isRange: boolean
|
||||
timer: number
|
||||
avgHeight: number
|
||||
monthsNum: number
|
||||
defaultRange: number[]
|
||||
}
|
||||
|
||||
export interface CalendarTaroState extends CalendarState {
|
||||
scrollTop: number
|
||||
containerHeight: string
|
||||
}
|
||||
|
||||
export interface CalendarDateProp {
|
||||
year: string
|
||||
month: string
|
||||
}
|
||||
|
||||
export interface Day {
|
||||
day: string
|
||||
type: string
|
||||
year?: string
|
||||
month?: string
|
||||
}
|
||||
|
||||
export interface MonthInfo {
|
||||
curData: string[] | string
|
||||
title: string
|
||||
monthData: Day[]
|
||||
cssHeight: number
|
||||
cssScrollHeight: number
|
||||
}
|
||||
|
||||
export interface CalendarInst extends HTMLElement {
|
||||
scrollToDate: (date: string) => void
|
||||
initPosition: () => void
|
||||
}
|
||||
47
uni_modules/nutui-uni/components/card/card.ts
Normal file
47
uni_modules/nutui-uni/components/card/card.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { commonProps, makeStringProp, truthProp } from '../_utils'
|
||||
|
||||
export const cardProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 左侧图片 `Url`
|
||||
*/
|
||||
imgUrl: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 标题
|
||||
*/
|
||||
title: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 商品价格
|
||||
*/
|
||||
price: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 会员价格
|
||||
*/
|
||||
vipPrice: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 店铺介绍
|
||||
*/
|
||||
shopDesc: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 配送方式
|
||||
*/
|
||||
delivery: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 店铺名称
|
||||
*/
|
||||
shopName: makeStringProp(''),
|
||||
|
||||
/**
|
||||
* @description 是否需要价格展示
|
||||
*/
|
||||
isNeedPrice: truthProp,
|
||||
}
|
||||
|
||||
export type CardProps = ExtractPropTypes<typeof cardProps>
|
||||
72
uni_modules/nutui-uni/components/card/card.vue
Normal file
72
uni_modules/nutui-uni/components/card/card.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import NutPrice from '../price/price.vue'
|
||||
import NutTag from '../tag/tag.vue'
|
||||
import { cardProps } from './card'
|
||||
|
||||
const props = defineProps(cardProps)
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-card`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :style="customStyle">
|
||||
<div class="nut-card__left">
|
||||
<image :src="imgUrl" alt="" />
|
||||
</div>
|
||||
<div class="nut-card__right">
|
||||
<div class="nut-card__right__title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot name="prolist" />
|
||||
<div v-if="isNeedPrice" class="nut-card__right__price">
|
||||
<slot name="price">
|
||||
<NutPrice v-if="price" :price="price" />
|
||||
</slot>
|
||||
<slot name="origin">
|
||||
<view class="nut-card__right__price__origin">
|
||||
<NutPrice v-if="vipPrice" :price="vipPrice" />
|
||||
</view>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="nut-card__right__other">
|
||||
<slot name="shopTag">
|
||||
<NutTag type="danger">
|
||||
{{ shopDesc }}
|
||||
</NutTag>
|
||||
<NutTag plain>
|
||||
{{ delivery }}
|
||||
</NutTag>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="nut-card__right__shop">
|
||||
<slot name="shopName">
|
||||
<div class="nut-card__right__shop__name">
|
||||
{{ shopName }}
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
112
uni_modules/nutui-uni/components/card/index.scss
Normal file
112
uni_modules/nutui-uni/components/card/index.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
@import '../price/index';
|
||||
@import '../tag/index';
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-card {
|
||||
.nut-card__right {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.nut-card__left {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background-color: $card-left-background-color;
|
||||
border-radius: $card-left-border-radius;
|
||||
|
||||
> image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-card__right {
|
||||
flex: 1;
|
||||
padding: 0 10px 8px;
|
||||
|
||||
.nut-card__right__title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.nut-card__right__price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
margin-top: 9px;
|
||||
line-height: 18px;
|
||||
|
||||
.nut-price {
|
||||
.nut-price--symbol-large {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nut-price--large {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.nut-price--decimal-large {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-card__right__price__origin {
|
||||
:deep(.nut-price) {
|
||||
margin-left: 2px;
|
||||
color: #d2a448;
|
||||
|
||||
.nut-price--symbol-large {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nut-price--large {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nut-price--decimal-large {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.nut-card__right__other {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0 2px;
|
||||
|
||||
.nut-tag {
|
||||
padding: 0 2px;
|
||||
margin-right: 5px;
|
||||
font-size: $card-font-size-0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-card__right__shop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
|
||||
.nut-card__right__shop__name {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/card/index.ts
Normal file
1
uni_modules/nutui-uni/components/card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './card'
|
||||
106
uni_modules/nutui-uni/components/cascader/cascader.ts
Normal file
106
uni_modules/nutui-uni/components/cascader/cascader.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { ExtractPropTypes, PropType, StyleValue } from 'vue'
|
||||
import {
|
||||
CHANGE_EVENT,
|
||||
CLOSE_EVENT,
|
||||
CLOSED_EVENT,
|
||||
OPEN_EVENT,
|
||||
OPENED_EVENT,
|
||||
UPDATE_MODEL_EVENT,
|
||||
UPDATE_VISIBLE_EVENT,
|
||||
} from '../_constants'
|
||||
import { commonProps, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
import { popupProps } from '../popup/popup'
|
||||
import type { CascaderOption, CascaderValue } from './types'
|
||||
|
||||
export const cascaderProps = {
|
||||
...popupProps,
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 选中值,双向绑定
|
||||
*/
|
||||
modelValue: Array,
|
||||
/**
|
||||
* @description 显示选择层
|
||||
*/
|
||||
visible: Boolean,
|
||||
/**
|
||||
* @description 级联数据
|
||||
*/
|
||||
options: makeArrayProp<any>([]),
|
||||
/**
|
||||
* @description 是否开启动态加载
|
||||
*/
|
||||
lazy: Boolean,
|
||||
/**
|
||||
* @description 动态加载回调,开启动态加载时生效
|
||||
*/
|
||||
lazyLoad: Function,
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `value` 的字段
|
||||
*/
|
||||
valueKey: makeStringProp('value'),
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `text` 的字段
|
||||
*/
|
||||
textKey: makeStringProp('text'),
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `children` 的字段
|
||||
*/
|
||||
childrenKey: makeStringProp('children'),
|
||||
/**
|
||||
* @description 当 `options` 为可转换为树形结构的扁平结构时,配置转换规则
|
||||
*/
|
||||
convertConfig: Object,
|
||||
/**
|
||||
* @description 是否需要弹层展示(设置为 `false` 后,`title` 失效)
|
||||
*/
|
||||
poppable: truthProp,
|
||||
/**
|
||||
* @description 标题
|
||||
*/
|
||||
title: String,
|
||||
/**
|
||||
* @description 选中底部展示样式 可选值: 'line', 'smile'
|
||||
*/
|
||||
titleType: makeStringProp<'line' | 'card' | 'smile'>('line'),
|
||||
/**
|
||||
* @description 标签栏字体尺寸大小 可选值: 'large', 'normal', 'small'
|
||||
*/
|
||||
titleSize: makeStringProp<'large' | 'normal' | 'small'>('normal'),
|
||||
/**
|
||||
* @description 标签间隙
|
||||
*/
|
||||
titleGutter: makeNumericProp(0),
|
||||
/**
|
||||
* @description 是否省略过长的标题文字
|
||||
*/
|
||||
titleEllipsis: truthProp,
|
||||
/**
|
||||
* @description 自定义弹窗样式
|
||||
*/
|
||||
popStyle: {
|
||||
type: [String, Object, Array] as PropType<StyleValue>,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description 遮罩显示时的背景是否锁定
|
||||
*/
|
||||
lockScroll: truthProp,
|
||||
}
|
||||
|
||||
export type CascaderProps = ExtractPropTypes<typeof cascaderProps>
|
||||
|
||||
/* eslint-disable unused-imports/no-unused-vars */
|
||||
export const cascaderEmits = {
|
||||
[UPDATE_MODEL_EVENT]: (value: CascaderValue) => true,
|
||||
[UPDATE_VISIBLE_EVENT]: (value: boolean) => true,
|
||||
[CHANGE_EVENT]: (value: CascaderValue, nodes: CascaderOption[]) => true,
|
||||
pathChange: (nodes: CascaderOption[]) => true,
|
||||
[OPEN_EVENT]: () => true,
|
||||
[OPENED_EVENT]: () => true,
|
||||
[CLOSE_EVENT]: () => true,
|
||||
[CLOSED_EVENT]: () => true,
|
||||
}
|
||||
/* eslint-enable unused-imports/no-unused-vars */
|
||||
|
||||
export type CascaderEmits = typeof cascaderEmits
|
||||
171
uni_modules/nutui-uni/components/cascader/cascader.vue
Normal file
171
uni_modules/nutui-uni/components/cascader/cascader.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, ref, useSlots, watch } from 'vue'
|
||||
import {
|
||||
CHANGE_EVENT,
|
||||
CLOSE_EVENT,
|
||||
CLOSED_EVENT,
|
||||
OPEN_EVENT,
|
||||
OPENED_EVENT,
|
||||
PREFIX,
|
||||
UPDATE_MODEL_EVENT,
|
||||
UPDATE_VISIBLE_EVENT,
|
||||
} from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import NutCascaderItem from '../cascaderitem/cascaderitem.vue'
|
||||
import NutPopup from '../popup/popup.vue'
|
||||
import { cascaderEmits, cascaderProps } from './cascader'
|
||||
import type { CascaderOption, CascaderValue } from './types'
|
||||
|
||||
const props = defineProps(cascaderProps)
|
||||
|
||||
const emit = defineEmits(cascaderEmits)
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const innerValue = ref(props.modelValue as CascaderValue)
|
||||
|
||||
const innerVisible = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(value) {
|
||||
emit(UPDATE_VISIBLE_EVENT, value)
|
||||
},
|
||||
})
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const popClasses = computed(() => {
|
||||
return `${componentName}__popup ${props.popClass}`
|
||||
})
|
||||
|
||||
const overlayClasses = computed(() => {
|
||||
return `${componentName}__overlay ${props.overlayClass}`
|
||||
})
|
||||
|
||||
function handleChange(value: CascaderValue, pathNodes: CascaderOption[]) {
|
||||
innerValue.value = value
|
||||
innerVisible.value = false
|
||||
|
||||
emit(UPDATE_MODEL_EVENT, value)
|
||||
emit(CHANGE_EVENT, value, pathNodes)
|
||||
}
|
||||
|
||||
function handlePathChange(pathNodes: CascaderOption[]) {
|
||||
emit('pathChange', pathNodes)
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
emit(OPEN_EVENT)
|
||||
}
|
||||
|
||||
function handleOpened() {
|
||||
emit(OPENED_EVENT)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit(CLOSE_EVENT)
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
emit(CLOSED_EVENT)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (value) => {
|
||||
if (value !== innerValue.value) {
|
||||
innerValue.value = value as CascaderValue
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-cascader`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="props.customStyle">
|
||||
<template v-if="props.poppable">
|
||||
<NutPopup
|
||||
v-model:visible="innerVisible"
|
||||
:custom-class="popClasses"
|
||||
:custom-style="props.popStyle"
|
||||
:overlay-class="overlayClasses"
|
||||
:overlay-style="props.overlayStyle"
|
||||
position="bottom"
|
||||
round
|
||||
:closeable="props.closeable"
|
||||
:close-icon="props.closeIcon"
|
||||
:close-icon-position="props.closeIconPosition"
|
||||
:z-index="props.zIndex"
|
||||
:lock-scroll="props.lockScroll"
|
||||
:overlay="props.overlay"
|
||||
:close-on-click-overlay="props.closeOnClickOverlay"
|
||||
:destroy-on-close="false"
|
||||
@open="handleOpen"
|
||||
@opened="handleOpened"
|
||||
@close="handleClose"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<slot v-if="slots.title" name="title" />
|
||||
|
||||
<template v-else>
|
||||
<rich-text v-if="props.title" class="nut-cascader__bar" :nodes="props.title" />
|
||||
</template>
|
||||
|
||||
<NutCascaderItem
|
||||
:model-value="innerValue"
|
||||
:visible="innerVisible"
|
||||
:options="props.options"
|
||||
:lazy="props.lazy"
|
||||
:lazy-load="props.lazyLoad"
|
||||
:value-key="props.valueKey"
|
||||
:text-key="props.textKey"
|
||||
:children-key="props.childrenKey"
|
||||
:convert-config="props.convertConfig"
|
||||
:title-type="props.titleType"
|
||||
:title-size="props.titleSize"
|
||||
:title-gutter="props.titleGutter"
|
||||
:title-ellipsis="props.titleEllipsis"
|
||||
@change="handleChange"
|
||||
@path-change="handlePathChange"
|
||||
/>
|
||||
</NutPopup>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<NutCascaderItem
|
||||
:model-value="innerValue"
|
||||
:visible="innerVisible"
|
||||
:options="props.options"
|
||||
:lazy="props.lazy"
|
||||
:lazy-load="props.lazyLoad"
|
||||
:value-key="props.valueKey"
|
||||
:text-key="props.textKey"
|
||||
:children-key="props.childrenKey"
|
||||
:convert-config="props.convertConfig"
|
||||
:title-type="props.titleType"
|
||||
:title-size="props.titleSize"
|
||||
:title-gutter="props.titleGutter"
|
||||
:title-ellipsis="props.titleEllipsis"
|
||||
@change="handleChange"
|
||||
@path-change="handlePathChange"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./index";
|
||||
@import "../popup/index";
|
||||
</style>
|
||||
80
uni_modules/nutui-uni/components/cascader/helper.ts
Normal file
80
uni_modules/nutui-uni/components/cascader/helper.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { CascaderConfig, CascaderOption, ConvertConfig } from './types'
|
||||
|
||||
export function formatTree(tree: CascaderOption[], parent: CascaderOption | null, config: CascaderConfig): CascaderOption[] {
|
||||
return tree.map((node: CascaderOption) => {
|
||||
const { value: valueKey = 'value', text: textKey = 'text', children: childrenKey = 'children' } = config
|
||||
|
||||
const { [valueKey]: value, [textKey]: text, [childrenKey]: children, ...others } = node
|
||||
|
||||
const newNode: CascaderOption = {
|
||||
loading: false,
|
||||
...others,
|
||||
level: parent ? ((parent && parent.level) || 0) + 1 : 0,
|
||||
value,
|
||||
text,
|
||||
children,
|
||||
_parent: parent,
|
||||
}
|
||||
|
||||
if (newNode.children && newNode.children.length)
|
||||
newNode.children = formatTree(newNode.children, newNode, config)
|
||||
|
||||
return newNode
|
||||
})
|
||||
}
|
||||
|
||||
export function eachTree(tree: CascaderOption[], cb: (node: CascaderOption) => any): void {
|
||||
let i = 0
|
||||
let node: CascaderOption
|
||||
/* eslint-disable no-cond-assign */
|
||||
while ((node = tree[i++])) {
|
||||
if (cb(node) === true)
|
||||
break
|
||||
|
||||
if (node.children && node.children.length)
|
||||
eachTree(node.children, cb)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultConvertConfig = {
|
||||
topId: null,
|
||||
idKey: 'id',
|
||||
pidKey: 'pid',
|
||||
sortKey: '',
|
||||
}
|
||||
|
||||
export function convertListToOptions(list: CascaderOption[], options: ConvertConfig): CascaderOption[] {
|
||||
const mergedOptions = {
|
||||
...defaultConvertConfig,
|
||||
...(options || {}),
|
||||
}
|
||||
|
||||
const { topId, idKey, pidKey, sortKey } = mergedOptions
|
||||
|
||||
let result: CascaderOption[] = []
|
||||
let map: any = {}
|
||||
|
||||
list.forEach((node: CascaderOption) => {
|
||||
node = { ...node }
|
||||
const { [idKey]: id, [pidKey]: pid } = node
|
||||
const children = (map[pid] = map[pid] || [])
|
||||
|
||||
if (!result.length && pid === topId)
|
||||
result = children
|
||||
|
||||
children.push(node)
|
||||
|
||||
node.children = map[id] || (map[id] = [])
|
||||
})
|
||||
|
||||
if (sortKey) {
|
||||
Object.keys(map).forEach((i) => {
|
||||
if (map[i].length > 1)
|
||||
map[i].sort((a: CascaderOption, b: CascaderOption) => a[sortKey] - b[sortKey])
|
||||
})
|
||||
}
|
||||
|
||||
map = null
|
||||
|
||||
return result
|
||||
}
|
||||
110
uni_modules/nutui-uni/components/cascader/index.scss
Normal file
110
uni_modules/nutui-uni/components/cascader/index.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
.nut-theme-dark {
|
||||
.nut-cascader {
|
||||
.nut-tabs__titles {
|
||||
background: $dark-background3 !important;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
color: $dark-color;
|
||||
background: $dark-background2;
|
||||
}
|
||||
|
||||
&-item {
|
||||
&__inner {
|
||||
color: $dark-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-cascader {
|
||||
.nut-tab-pane {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nut-tabs__titles {
|
||||
padding: $cascader-tabs-item-padding;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&-item {
|
||||
width: 100%;
|
||||
font-size: $cascader-font-size;
|
||||
line-height: $cascader-line-height;
|
||||
|
||||
$block: &;
|
||||
|
||||
&.nut-tabs {
|
||||
&.horizontal {
|
||||
.nut-tabs__titles {
|
||||
.nut-tabs__titles-item {
|
||||
flex: initial;
|
||||
padding: $cascader-tabs-item-padding;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $cascader-item-padding;
|
||||
margin: 0;
|
||||
font-size: $cascader-item-font-size;
|
||||
color: $cascader-item-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__icon-check {
|
||||
margin-left: 10px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__icon-loading {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&:not(.disabled) {
|
||||
color: $cascader-item-active-color;
|
||||
}
|
||||
|
||||
#{$block}__icon-check {
|
||||
color: $cascader-item-active-color;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $cascader-bar-padding;
|
||||
font-size: $cascader-bar-font-size;
|
||||
font-weight: bold;
|
||||
line-height: $cascader-bar-line-height;
|
||||
color: $cascader-bar-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-pane {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 342px;
|
||||
padding: 10px 0 0;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/cascader/index.ts
Normal file
1
uni_modules/nutui-uni/components/cascader/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './cascader'
|
||||
73
uni_modules/nutui-uni/components/cascader/tree.ts
Normal file
73
uni_modules/nutui-uni/components/cascader/tree.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { eachTree, formatTree } from './helper'
|
||||
import type { CascaderConfig, CascaderOption, CascaderValue } from './types'
|
||||
|
||||
class Tree {
|
||||
nodes: CascaderOption[]
|
||||
readonly config: CascaderConfig
|
||||
|
||||
constructor(nodes: CascaderOption[], config?: CascaderConfig) {
|
||||
this.config = {
|
||||
value: 'value',
|
||||
text: 'text',
|
||||
children: 'children',
|
||||
...(config || {}),
|
||||
}
|
||||
this.nodes = formatTree(nodes, null, this.config)
|
||||
}
|
||||
|
||||
updateChildren(nodes: CascaderOption[], parent: CascaderOption | null): void {
|
||||
if (!parent)
|
||||
this.nodes = formatTree(nodes, null, this.config)
|
||||
|
||||
else
|
||||
parent.children = formatTree(nodes, parent, this.config)
|
||||
}
|
||||
|
||||
// for test
|
||||
getNodeByValue(value: CascaderOption['value']): CascaderOption | void {
|
||||
let foundNode
|
||||
eachTree(this.nodes, (node: CascaderOption) => {
|
||||
if (node.value === value) {
|
||||
foundNode = node
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
getPathNodesByValue(value: CascaderValue): CascaderOption[] {
|
||||
if (!value.length)
|
||||
return []
|
||||
|
||||
const pathNodes = []
|
||||
let currentNodes: CascaderOption[] | void = this.nodes
|
||||
|
||||
while (currentNodes && currentNodes.length) {
|
||||
const foundNode: CascaderOption | void = currentNodes.find(node => node.value === value[node.level as number])
|
||||
|
||||
if (!foundNode)
|
||||
break
|
||||
|
||||
pathNodes.push(foundNode)
|
||||
currentNodes = foundNode.children
|
||||
}
|
||||
|
||||
return pathNodes
|
||||
}
|
||||
|
||||
isLeaf(node: CascaderOption, lazy: boolean): boolean {
|
||||
const { leaf, children } = node
|
||||
const hasChildren = Array.isArray(children) && Boolean(children.length)
|
||||
|
||||
return leaf == null ? !hasChildren && !lazy : leaf
|
||||
}
|
||||
|
||||
hasChildren(node: CascaderOption, lazy: boolean): boolean {
|
||||
if (lazy)
|
||||
return Array.isArray(node.children) && Boolean(node.children.length)
|
||||
return !this.isLeaf(node, lazy)
|
||||
}
|
||||
}
|
||||
|
||||
export default Tree
|
||||
37
uni_modules/nutui-uni/components/cascader/types.ts
Normal file
37
uni_modules/nutui-uni/components/cascader/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface CascaderPane {
|
||||
nodes: CascaderOption[]
|
||||
selectedNode: CascaderOption | null
|
||||
}
|
||||
|
||||
export interface CascaderConfig {
|
||||
value?: string
|
||||
text?: string
|
||||
children?: string
|
||||
}
|
||||
|
||||
export interface CascaderTabs {
|
||||
title: string
|
||||
paneKey: string
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export interface CascaderOption {
|
||||
text?: string
|
||||
value?: number | string
|
||||
disabled?: boolean
|
||||
children?: CascaderOption[]
|
||||
leaf?: boolean
|
||||
level?: number
|
||||
loading?: boolean
|
||||
|
||||
[key: PropertyKey]: any
|
||||
}
|
||||
|
||||
export type CascaderValue = CascaderOption['value'][]
|
||||
|
||||
export interface ConvertConfig {
|
||||
topId?: string | number | null
|
||||
idKey?: string
|
||||
pidKey?: string
|
||||
sortKey?: string
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import { commonProps, makeArrayProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
import type { CascaderOption, CascaderValue } from '../cascader/types'
|
||||
|
||||
export const cascaderitemProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 选中值,双向绑定
|
||||
*/
|
||||
modelValue: Array,
|
||||
/**
|
||||
* @description 显示选择层
|
||||
*/
|
||||
visible: Boolean,
|
||||
/**
|
||||
* @description 级联数据
|
||||
*/
|
||||
options: makeArrayProp<any>([]),
|
||||
/**
|
||||
* @description 是否开启动态加载
|
||||
*/
|
||||
lazy: Boolean,
|
||||
/**
|
||||
* @description 动态加载回调,开启动态加载时生效
|
||||
*/
|
||||
lazyLoad: Function,
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `value` 的字段
|
||||
*/
|
||||
valueKey: makeStringProp('value'),
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `text` 的字段
|
||||
*/
|
||||
textKey: makeStringProp('text'),
|
||||
/**
|
||||
* @description 自定义 `options` 结构中 `children` 的字段
|
||||
*/
|
||||
childrenKey: makeStringProp('children'),
|
||||
/**
|
||||
* @description 当 `options` 为可转换为树形结构的扁平结构时,配置转换规则
|
||||
*/
|
||||
convertConfig: Object,
|
||||
/**
|
||||
* @description 选中底部展示样式 可选值: 'line', 'smile'
|
||||
*/
|
||||
titleType: makeStringProp<'line' | 'card' | 'smile'>('line'),
|
||||
/**
|
||||
* @description 标签栏字体尺寸大小 可选值: 'large', 'normal', 'small'
|
||||
*/
|
||||
titleSize: makeStringProp<'large' | 'normal' | 'small'>('normal'),
|
||||
/**
|
||||
* @description 标签间隙
|
||||
*/
|
||||
titleGutter: makeNumericProp(0),
|
||||
/**
|
||||
* @description 是否省略过长的标题文字
|
||||
*/
|
||||
titleEllipsis: truthProp,
|
||||
}
|
||||
|
||||
export type CascaderItemProps = ExtractPropTypes<typeof cascaderitemProps>
|
||||
|
||||
/* eslint-disable unused-imports/no-unused-vars */
|
||||
export const cascaderitemEmits = {
|
||||
[UPDATE_MODEL_EVENT]: (value: CascaderValue) => true,
|
||||
[CHANGE_EVENT]: (value: CascaderValue, nodes: CascaderOption[]) => true,
|
||||
pathChange: (value: CascaderOption[]) => true,
|
||||
}
|
||||
/* eslint-enable unused-imports/no-unused-vars */
|
||||
|
||||
export type CascaderItemEmits = typeof cascaderitemEmits
|
||||
339
uni_modules/nutui-uni/components/cascaderitem/cascaderitem.vue
Normal file
339
uni_modules/nutui-uni/components/cascaderitem/cascaderitem.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, ref, watch } from 'vue'
|
||||
import { CHANGE_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import { useTranslate } from '../../locale'
|
||||
import { convertListToOptions } from '../cascader/helper'
|
||||
import Tree from '../cascader/tree'
|
||||
import type { CascaderOption, CascaderPane, CascaderTabs, CascaderValue, ConvertConfig } from '../cascader/types'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutTabPane from '../tabpane/tabpane.vue'
|
||||
import NutTabs from '../tabs/tabs.vue'
|
||||
import { cascaderitemEmits, cascaderitemProps } from './cascaderitem'
|
||||
|
||||
const props = defineProps(cascaderitemProps)
|
||||
|
||||
const emit = defineEmits(cascaderitemEmits)
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
|
||||
const configs = computed(() => ({
|
||||
lazy: props.lazy,
|
||||
lazyLoad: props.lazyLoad,
|
||||
valueKey: props.valueKey,
|
||||
textKey: props.textKey,
|
||||
childrenKey: props.childrenKey,
|
||||
convertConfig: props.convertConfig,
|
||||
}))
|
||||
|
||||
const tabsCursor = ref(0)
|
||||
const initLoading = ref(false)
|
||||
|
||||
const innerValue = ref(props.modelValue as CascaderValue)
|
||||
|
||||
const tree = ref(new Tree([], {}))
|
||||
|
||||
const panes = ref<CascaderPane[]>([])
|
||||
|
||||
const isLazy = computed(() => configs.value.lazy && Boolean(configs.value.lazyLoad))
|
||||
const lazyLoadMap = new Map()
|
||||
|
||||
let currentProcessNode: CascaderOption | null
|
||||
|
||||
async function init() {
|
||||
lazyLoadMap.clear()
|
||||
|
||||
panes.value = []
|
||||
tabsCursor.value = 0
|
||||
initLoading.value = false
|
||||
currentProcessNode = null
|
||||
|
||||
let { options } = props
|
||||
|
||||
if (configs.value.convertConfig) {
|
||||
options = convertListToOptions(options as CascaderOption[], configs.value.convertConfig as ConvertConfig)
|
||||
}
|
||||
|
||||
tree.value = new Tree(options as CascaderOption[], {
|
||||
value: configs.value.valueKey,
|
||||
text: configs.value.textKey,
|
||||
children: configs.value.childrenKey,
|
||||
})
|
||||
|
||||
if (isLazy.value && !tree.value.nodes.length) {
|
||||
await invokeLazyLoad({
|
||||
root: true,
|
||||
loading: true,
|
||||
text: '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
|
||||
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
|
||||
|
||||
syncValue()
|
||||
}
|
||||
|
||||
const methods = {
|
||||
// 选中一个节点,静默模式不触发事件
|
||||
async handleNode(node: CascaderOption, silent?: boolean) {
|
||||
const { disabled, loading } = node
|
||||
|
||||
if ((!silent && disabled) || !panes.value[tabsCursor.value])
|
||||
return
|
||||
|
||||
if (tree.value.isLeaf(node, isLazy.value)) {
|
||||
node.leaf = true
|
||||
panes.value[tabsCursor.value].selectedNode = node
|
||||
panes.value = panes.value.slice(0, (node.level as number) + 1)
|
||||
|
||||
if (!silent) {
|
||||
const pathNodes = panes.value.map(pane => pane.selectedNode)
|
||||
|
||||
emitChange(pathNodes as CascaderOption[])
|
||||
emit('pathChange', pathNodes as CascaderOption[])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (tree.value.hasChildren(node, isLazy.value)) {
|
||||
const level = (node.level as number) + 1
|
||||
|
||||
panes.value[tabsCursor.value].selectedNode = node
|
||||
panes.value = panes.value.slice(0, level)
|
||||
panes.value.push({
|
||||
nodes: node.children || [],
|
||||
selectedNode: null,
|
||||
})
|
||||
|
||||
tabsCursor.value = level
|
||||
|
||||
if (!silent) {
|
||||
const pathNodes = panes.value.map(pane => pane.selectedNode)
|
||||
emit('pathChange', pathNodes as CascaderOption[])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
currentProcessNode = node
|
||||
|
||||
if (loading)
|
||||
return
|
||||
|
||||
await invokeLazyLoad(node)
|
||||
|
||||
if (currentProcessNode === node) {
|
||||
panes.value[tabsCursor.value].selectedNode = node
|
||||
methods.handleNode(node, silent)
|
||||
}
|
||||
},
|
||||
handleTabClick(tab: CascaderTabs) {
|
||||
currentProcessNode = null
|
||||
tabsCursor.value = Number(tab.paneKey)
|
||||
},
|
||||
|
||||
isSelected(pane: CascaderPane, node: CascaderOption) {
|
||||
return pane?.selectedNode?.value === node.value
|
||||
},
|
||||
}
|
||||
|
||||
async function syncValue() {
|
||||
const currentValue = innerValue.value
|
||||
|
||||
if (currentValue === undefined || !tree.value.nodes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentValue.length === 0) {
|
||||
tabsCursor.value = 0
|
||||
panes.value = [{ nodes: tree.value.nodes, selectedNode: null }]
|
||||
return
|
||||
}
|
||||
|
||||
let needToSync = currentValue
|
||||
|
||||
if (isLazy.value && Array.isArray(currentValue) && currentValue.length) {
|
||||
needToSync = []
|
||||
const parent = tree.value.nodes.find(node => node.value === currentValue[0])
|
||||
|
||||
if (parent) {
|
||||
needToSync = [parent.value]
|
||||
initLoading.value = true
|
||||
|
||||
const last = await currentValue.slice(1).reduce(async (p: Promise<CascaderOption | void>, value) => {
|
||||
const parent = await p
|
||||
|
||||
await invokeLazyLoad(parent)
|
||||
const node = parent?.children?.find(item => item.value === value)
|
||||
|
||||
if (node)
|
||||
needToSync.push(value)
|
||||
|
||||
return Promise.resolve(node)
|
||||
}, Promise.resolve(parent))
|
||||
|
||||
await invokeLazyLoad(last)
|
||||
|
||||
initLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (needToSync.length && currentValue === props.modelValue) {
|
||||
const pathNodes = tree.value.getPathNodesByValue(needToSync)
|
||||
pathNodes.forEach((node, index) => {
|
||||
tabsCursor.value = index
|
||||
methods.handleNode(node, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeLazyLoad(node?: CascaderOption | void) {
|
||||
if (!node)
|
||||
return
|
||||
|
||||
if (!configs.value.lazyLoad) {
|
||||
node.leaf = true
|
||||
return
|
||||
}
|
||||
|
||||
if (tree.value.isLeaf(node, isLazy.value) || tree.value.hasChildren(node, isLazy.value))
|
||||
return
|
||||
|
||||
node.loading = true
|
||||
|
||||
const parent = node.root ? null : node
|
||||
let lazyLoadPromise = lazyLoadMap.get(node)
|
||||
|
||||
if (!lazyLoadPromise) {
|
||||
lazyLoadPromise = new Promise((resolve) => {
|
||||
// 外部必须resolve
|
||||
configs.value.lazyLoad?.(node, resolve)
|
||||
})
|
||||
lazyLoadMap.set(node, lazyLoadPromise)
|
||||
}
|
||||
|
||||
const nodes: CascaderOption[] | void = await lazyLoadPromise
|
||||
|
||||
if (Array.isArray(nodes) && nodes.length > 0) {
|
||||
tree.value.updateChildren(nodes, parent)
|
||||
}
|
||||
else {
|
||||
// 如果加载完成后没有提供子节点,作为叶子节点处理
|
||||
node.leaf = true
|
||||
}
|
||||
|
||||
node.loading = false
|
||||
lazyLoadMap.delete(node)
|
||||
}
|
||||
|
||||
function emitChange(pathNodes: CascaderOption[]) {
|
||||
const emitValue = pathNodes.map(node => node.value)
|
||||
|
||||
innerValue.value = emitValue
|
||||
|
||||
emit(UPDATE_MODEL_EVENT, emitValue)
|
||||
emit(CHANGE_EVENT, emitValue, pathNodes)
|
||||
}
|
||||
|
||||
function formatTabTitle(pane: CascaderPane) {
|
||||
return pane.selectedNode ? pane.selectedNode.text : translate('select')
|
||||
}
|
||||
|
||||
watch(() => [configs.value, props.options], () => {
|
||||
init()
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (value) => {
|
||||
if (value !== innerValue.value) {
|
||||
innerValue.value = value as CascaderValue
|
||||
|
||||
syncValue()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.visible, (value) => {
|
||||
// TODO: value为空时,保留上次选择记录,修复单元测试问题
|
||||
if (value && Array.isArray(innerValue.value) && innerValue.value.length > 0) {
|
||||
syncValue()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-cascader-item`
|
||||
|
||||
const { translate } = useTranslate(componentName)
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NutTabs
|
||||
v-model="tabsCursor"
|
||||
:custom-class="classes"
|
||||
:custom-style="props.customStyle"
|
||||
:type="props.titleType"
|
||||
:size="props.titleSize"
|
||||
:title-gutter="props.titleGutter"
|
||||
:ellipsis="props.titleEllipsis"
|
||||
title-scroll
|
||||
@click="methods.handleTabClick"
|
||||
>
|
||||
<template v-if="!initLoading && panes.length">
|
||||
<NutTabPane v-for="(pane, index) in panes" :key="index" :title="formatTabTitle(pane)">
|
||||
<view class="nut-cascader-pane" role="menu">
|
||||
<scroll-view style="height: 100%" :scroll-y="true">
|
||||
<template v-for="node in pane.nodes" :key="node.value">
|
||||
<view
|
||||
class="nut-cascader-item__inner"
|
||||
:class="{ active: methods.isSelected(pane, node), disabled: node.disabled }"
|
||||
role="menuitemradio"
|
||||
:aria-checked="methods.isSelected(pane, node)"
|
||||
:aria-disabled="node.disabled || undefined"
|
||||
@click="methods.handleNode(node, false)"
|
||||
>
|
||||
<view class="nut-cascader-item__title">
|
||||
{{ node.text }}
|
||||
</view>
|
||||
|
||||
<NutIcon
|
||||
v-if="node.loading"
|
||||
custom-class="nut-cascader-item__icon-loading"
|
||||
loading
|
||||
name="loading"
|
||||
/>
|
||||
<NutIcon
|
||||
v-else
|
||||
custom-class="nut-cascader-item__icon-check"
|
||||
name="checklist"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</NutTabPane>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<NutTabPane title="Loading...">
|
||||
<view class="nut-cascader-pane" />
|
||||
</NutTabPane>
|
||||
</template>
|
||||
</NutTabs>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./index";
|
||||
</style>
|
||||
3
uni_modules/nutui-uni/components/cascaderitem/index.scss
Normal file
3
uni_modules/nutui-uni/components/cascaderitem/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import "../cascader/index";
|
||||
@import "../tabs/index";
|
||||
@import "../tabpane/index";
|
||||
1
uni_modules/nutui-uni/components/cascaderitem/index.ts
Normal file
1
uni_modules/nutui-uni/components/cascaderitem/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './cascaderitem'
|
||||
29
uni_modules/nutui-uni/components/category/category.ts
Normal file
29
uni_modules/nutui-uni/components/category/category.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import { CHANGE_EVENT } from '../_constants'
|
||||
import { commonProps, isNumber, makeArrayProp, makeStringProp } from '../_utils'
|
||||
|
||||
export interface CategoryType {
|
||||
catName?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const categoryProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 分类模式:`classify`,`text`,`custom`
|
||||
*/
|
||||
type: makeStringProp<'classify' | 'text' | 'custom'>('classify'),
|
||||
|
||||
/**
|
||||
* @description 左侧导航栏数据列表
|
||||
*/
|
||||
category: makeArrayProp<any>([]),
|
||||
}
|
||||
|
||||
export type CategoryProps = ExtractPropTypes<typeof categoryProps>
|
||||
|
||||
export const categoryEmits = {
|
||||
[CHANGE_EVENT]: (index: number) => isNumber(index),
|
||||
}
|
||||
|
||||
export type CategoryEmits = typeof categoryEmits
|
||||
53
uni_modules/nutui-uni/components/category/category.vue
Normal file
53
uni_modules/nutui-uni/components/category/category.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import { CHANGE_EVENT, PREFIX } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import { categoryEmits, categoryProps } from './category'
|
||||
|
||||
const props = defineProps(categoryProps)
|
||||
const emit = defineEmits(categoryEmits)
|
||||
const checkIndex = ref(0)
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
function getChildList(index: any) {
|
||||
checkIndex.value = index
|
||||
emit(CHANGE_EVENT, index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-category`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :style="customStyle">
|
||||
<div class="nut-category__cateList">
|
||||
<div v-if="type === 'classify' || type === 'text'">
|
||||
<div v-for="(item, index) in category" :key="index" class="nut-category__cateListLeft">
|
||||
<div
|
||||
:class="[checkIndex === index ? 'nut-category__cateListItemChecked' : 'nut-category__cateListItem']"
|
||||
@click="getChildList(index)"
|
||||
>
|
||||
{{ item.catName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
64
uni_modules/nutui-uni/components/category/index.scss
Normal file
64
uni_modules/nutui-uni/components/category/index.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
.nut-theme-dark {
|
||||
.nut-category {
|
||||
&__cateList {
|
||||
background: $dark-background2;
|
||||
}
|
||||
|
||||
&__cateListLeft {
|
||||
background: $dark-background4;
|
||||
}
|
||||
|
||||
&__cateListItem {
|
||||
color: $dark-color-gray;
|
||||
}
|
||||
|
||||
&__cateListItemChecked {
|
||||
color: $dark-color;
|
||||
background: $dark-background2;
|
||||
|
||||
&::before {
|
||||
background: $category-list-item-checked-img-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-category {
|
||||
&__cateList {
|
||||
display: flex;
|
||||
background: $category-bg-color;
|
||||
}
|
||||
|
||||
&__cateListLeft {
|
||||
background: $category-list-left-bg-color;
|
||||
}
|
||||
|
||||
&__cateListItemChecked,
|
||||
&__cateListItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: $category-list-item-color;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
&__cateListItemChecked {
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
background: $category-list-item-checked-color;
|
||||
transition: all 0.3s;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 5px;
|
||||
height: 18px;
|
||||
content: "";
|
||||
background: $category-list-item-checked-img-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user