diff --git a/packages/opendesign/src/components/cascader/OCascader.vue b/packages/opendesign/src/components/cascader/OCascader.vue new file mode 100644 index 0000000000000000000000000000000000000000..d911bb95368586e1c9a89235b58c0b2b01bbd168 --- /dev/null +++ b/packages/opendesign/src/components/cascader/OCascader.vue @@ -0,0 +1,130 @@ + + + diff --git a/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue b/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue new file mode 100644 index 0000000000000000000000000000000000000000..21a54495189a9c9233ff80bfe0e013a1e23ad892 --- /dev/null +++ b/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue @@ -0,0 +1,119 @@ + + + diff --git a/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue b/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue new file mode 100644 index 0000000000000000000000000000000000000000..51dd708aa207c272b9f2347ba2123af35344141e --- /dev/null +++ b/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue @@ -0,0 +1,10 @@ + + + diff --git a/packages/opendesign/src/components/cascader/cascader.ts b/packages/opendesign/src/components/cascader/cascader.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae9a58c7934551ab3545f96ac068ea1d6b6bcaad --- /dev/null +++ b/packages/opendesign/src/components/cascader/cascader.ts @@ -0,0 +1,149 @@ +import { isArray, isUndefined } from '../_shared/is'; +import { CascaderValueT } from './types'; +import { CascaderOptionT } from './types'; + +interface CascaderNodeT { + value: string | number; + label?: string; + depth: number; + parent: CascaderNodeT | null; + children: CascaderNodeT[]; + isLeaf: boolean; +} + +interface ColumnInfoT { + value: string | number; + label?: string; + depth: number; + isLeaf: boolean; + isActive: boolean; +} + +const DFS = (options: Array, parentNode: CascaderNodeT, depth: number) => { + for (let i = 0, len = options.length; i < len; i++) { + const item = options[i]; + let node: CascaderNodeT = { + value: item.value, + label: item.label, + parent: parentNode, + depth: depth + 1, + children: [], + isLeaf: true, + }; + parentNode.children.push(node); + + if (item.children && item.children.length) { + node.isLeaf = false; + DFS(item.children, node, depth + 1); + } + } +}; + +export default class CascaderTree { + root: CascaderNodeT; + constructor() { + this.root = { + value: NaN, + label: '', + depth: 0, + parent: null, + children: [], + isLeaf: true, + }; + } + + updateTree(options: Array) { + this.root = { + value: NaN, + label: '', + depth: 0, + parent: null, + children: [], + isLeaf: true, + }; + DFS(options, this.root, 0); + } + + getNode(node: CascaderNodeT, val: string | number): CascaderNodeT | undefined { + if (node.value === val) { + return node; + } + + const children: Array = node.children; + + for (let i = 0, len = children.length; i < len; i++) { + const rlt = this.getNode(children[i], val); + if (rlt) { + return rlt; + } + } + } + + getChild(node: CascaderNodeT, val: string | number): CascaderNodeT | undefined { + const children: Array = node.children; + return children.find((item) => item.value === val); + } + + getPanelInfo(val: CascaderValueT | undefined) { + let rlt: Array> = []; + + if (isUndefined(val)) { + return rlt; + } + + if (!isArray(val)) { + let node = this.getNode(this.root, val); + if (isUndefined(node)) { + const columnInfo = this.getNextColumnInfo(this.root); + if (!isUndefined(columnInfo)) { + rlt = [columnInfo]; + } + } else { + while (node.parent) { + const columnInfo = this.getNextColumnInfo(node.parent, node.value); + if (!isUndefined(columnInfo)) { + rlt.unshift(columnInfo); + } + node = node.parent; + } + } + } else { + let parent = this.root; + + for (let i = 0, len = val.length; i < len; i++) { + const child = this.getChild(parent, val[i]); + if (isUndefined(child)) { + const columnInfo = this.getNextColumnInfo(this.root); + if (!isUndefined(columnInfo)) { + rlt = [columnInfo]; + } + break; + } else { + const columnInfo = this.getNextColumnInfo(parent, val[i]); + rlt.push(columnInfo); + parent = child; + } + } + } + + return rlt; + } + + getNextColumnInfo(node: CascaderNodeT, activeVal?: string | number): Array { + return node.children.map((item) => { + const rlt = { + value: item.value, + label: item.label, + depth: item.depth, + isActive: false, + isLeaf: item.children && item.children.length ? false : true, + }; + + if (!isUndefined(activeVal)) { + rlt.isActive = item.value === activeVal; + } + + return rlt; + }); + } +} diff --git a/packages/opendesign/src/components/cascader/index.ts b/packages/opendesign/src/components/cascader/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b67087af27e08fc1ee0784b94c0725fea6abf28 --- /dev/null +++ b/packages/opendesign/src/components/cascader/index.ts @@ -0,0 +1,13 @@ +import type { App } from 'vue'; + +import _OCascader from './OCascader.vue'; + +const OCascader = Object.assign(_OCascader, { + install(app: App) { + app.component(_OCascader.name, _OCascader); + }, +}); + +export * from './types'; + +export { OCascader }; diff --git a/packages/opendesign/src/components/cascader/style/index.scss b/packages/opendesign/src/components/cascader/style/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..8f59c84af2885157e68f789163299bc9f9a59347 --- /dev/null +++ b/packages/opendesign/src/components/cascader/style/index.scss @@ -0,0 +1,62 @@ +@use './var.scss'; + +.o-cascader-panel { + position: relative; + display: inline-flex; + border-radius: var(--o-radius-control-l); + height: 200px; +} + +.o-cascader-options { + list-style: none; + margin: 0; + padding: 4px 4px 4px 4px; + min-width: 144px; + max-width: 192px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + & + .o-cascader-options { + border-left: 1px solid var(--cascader-options-bd-clor); + } + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } +} + +.o-cascader-option { + display: flex; + align-items: center; + padding: var(--cascader-option-padding); + + color: var(--cascader-option-color); + border-radius: var(--cascader-option-radius); + background-color: var(--cascader-option-bg-color); + transition: background-color var(--o-duration-s) var(--o-easing-standard); + cursor: pointer; + + &:not(.o-cascader-option-active):hover { + background-color: var(--cascader-option-bg-color-hover); + } +} + +.o-cascader-option-label { + font-size: var(--cascader-option-text-size); + line-height: var(--cascader-option-text-height); +} + +.o-cascader-option-arrow { + font-size: var(--cascader-option-icon-size); + margin-left: auto; +} + +.o-cascader-option-active { + background-color: var(--cascader-option-bg-color-active); + font-weight: 500; +} diff --git a/packages/opendesign/src/components/cascader/style/index.ts b/packages/opendesign/src/components/cascader/style/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..33107e5d2c59f2090e3e52d9cf07964e7175ca10 --- /dev/null +++ b/packages/opendesign/src/components/cascader/style/index.ts @@ -0,0 +1,3 @@ +import '../../style'; +import '../../select/style'; +import './index.scss'; diff --git a/packages/opendesign/src/components/cascader/style/var.scss b/packages/opendesign/src/components/cascader/style/var.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d84c776c555bd2231db41ace2c458eaa801fc634 100644 --- a/packages/opendesign/src/components/cascader/style/var.scss +++ b/packages/opendesign/src/components/cascader/style/var.scss @@ -0,0 +1,18 @@ +.o-cascader-options { + --cascader-options-bd-clor: var(--o-color-control1-light); +} + +.o-cascader-option { + --cascader-option-color: var(--o-color-info1); + --cascader-option-text-size: var(--o-font_size-text1); + --cascader-option-text-height: var(--o-line_height-text1); + + --cascader-option-padding: 8px 12px; + --cascader-option-radius: var(--o-radius-control-m); + + --cascader-option-bg-color: transparent; + --cascader-option-bg-color-hover: var(--o-color-control2-light); + --cascader-option-bg-color-active: var(--o-color-control3-light); + + --cascader-option-icon-size: var(--o-icon_size-xs); +} diff --git a/packages/opendesign/src/components/cascader/types.ts b/packages/opendesign/src/components/cascader/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..637fc7b4e66393f2b8a2537e1b659ee3dc404397 --- /dev/null +++ b/packages/opendesign/src/components/cascader/types.ts @@ -0,0 +1,96 @@ +import { ExtractPropTypes, PropType } from 'vue'; +import { PopupPositionT, PopupTriggerT } from '../popup'; +import type { RoundT, VariantT } from '../_shared/global'; + +export type CascaderNodeValueT = string | number; +export type CascaderNodePathT = Array; +export type CascaderValueT = CascaderNodeValueT | CascaderNodePathT; + +export type CascaderOptionT = { + value: CascaderNodeValueT; + label?: string; + children?: CascaderOptionT[]; +}; + +export const cascaderProps = { + /** + * 级联选择器的值 + * v-model + */ + modelValue: { + type: [String, Number, Array] as PropType, + }, + /** + * 级联选择器选项值 + * + * */ + options: { + type: Array as PropType>, + }, + /** + * 是否使用路径模式 + * + * */ + pathMode: { + type: Boolean, + default: false, + }, + /** + * 圆角值 + */ + round: { + type: String as PropType, + }, + /** + * 级联选择器类型 + */ + variant: { + type: String as PropType, + default: 'outline', + }, + /** + * 提示文本 + */ + placeholder: { + type: String, + default: 'please select...', + }, + /** + * 下拉选项触发方式 + */ + trigger: { + type: String as PropType, + default: 'click', + }, + /** + * 下拉选项位置 + */ + optionPosition: { + type: String as PropType, + default: 'bl', + }, + /** + * 下拉选项宽度自适应规则 + * 'auto':自动 | 'min-width':最小宽度与选择框一致 | 'width': 宽度与选择框一致 + */ + optionWidthMode: { + type: String as PropType<'auto' | 'min-width' | 'width'>, + default: 'min-width', + }, + /** + * 是否在结束选择时,卸载下拉选项 + * v-model + */ + unmountOnHide: { + type: Boolean, + default: true, + }, + /** + * 过渡名称 + */ + transition: { + type: String, + }, +}; + +export type CascaderPropsT = ExtractPropTypes; diff --git a/packages/portal/src/router.ts b/packages/portal/src/router.ts index 4848a27a4f82a518be22ee501fcb0d33a10a395b..eb5ee782df42285b6e56ab22ca8d65092d8f4373 100644 --- a/packages/portal/src/router.ts +++ b/packages/portal/src/router.ts @@ -68,12 +68,12 @@ export const routes = [ label: '下拉框', component: () => import('@components/select/__demo__/IndexSelect.vue'), }, - // { - // path: '/cascader', - // name: 'Cascader', - // label: '级联选择器', - // component: () => import('@components/cascader/__demo__/IndexCascader.vue'), - // }, + { + path: '/cascader', + name: 'Cascader', + label: '级联选择器', + component: () => import('@components/cascader/__demo__/IndexCascader.vue'), + }, { path: '/radio', name: 'Radio',