init
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user