diff --git a/devui/cascader/components/cascader-item/index.scss b/devui/cascader/components/cascader-item/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..459a91366bdb41b186b4a21c989d59286dfb9404 --- /dev/null +++ b/devui/cascader/components/cascader-item/index.scss @@ -0,0 +1,22 @@ +@import '../../../style/mixins/flex'; +@import '../../../style/theme/color'; +@import '../../../style/core/font'; + +.devui-cascader-li { + &.devui-dropdown-item { + height: 32px; + padding: 8px 12px; + color: $devui-text; + cursor: pointer; + @include flex(flex-start); + } + + .dropdown-item-label { + display: inline-block; + flex: 1; + width: 0; + overflow: hidden; + text-overflow: ellipsis; + font-size: $devui-font-size; + } +} diff --git a/devui/cascader/components/cascader-item/index.tsx b/devui/cascader/components/cascader-item/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5182dd221b1773743c2b7bf4a48cd6ff4f27aa9f --- /dev/null +++ b/devui/cascader/components/cascader-item/index.tsx @@ -0,0 +1,21 @@ +import { getRootClass } from './use-class' +import { optionsHandles } from '../../hooks/use-cascader-options' +import './index.scss' +export const DCascaderItem = (props) => { + const { cascaderli, ulIndex } = props + const { changeCascaderIndexs } = optionsHandles() + const rootClasses = getRootClass() + const mouseHover = () => { + changeCascaderIndexs(cascaderli, ulIndex) + } + return ( +
  • + + { + cascaderli?.children?.length > 0 && + } +
  • + ) +} diff --git a/devui/cascader/components/cascader-item/use-class.ts b/devui/cascader/components/cascader-item/use-class.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a5ec3e21b6bd16e8fafc7da41ea55a592f0760b --- /dev/null +++ b/devui/cascader/components/cascader-item/use-class.ts @@ -0,0 +1,11 @@ +/** + * 定义组件class + */ + import { computed, ComputedRef } from 'vue'; + + export const getRootClass = (): ComputedRef => { + return computed(() => ({ + 'devui-cascader-li devui-dropdown-item': true, + })) + } + \ No newline at end of file diff --git a/devui/cascader/components/cascader-list/cascader-list-types.ts b/devui/cascader/components/cascader-list/cascader-list-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..85e2152df95d7ff7843a6db28e084074b8c38f24 --- /dev/null +++ b/devui/cascader/components/cascader-list/cascader-list-types.ts @@ -0,0 +1,23 @@ +import type { PropType, ExtractPropTypes } from 'vue' +import { CascaderItem } from '../../src/cascader-types' +export const cascaderulProps = { + /** + * 每个ul中的li + * @type {CascaderItem[]} + * @default [] + */ + cascaderlis: { + type: Array as PropType, + default: [], + }, + /** + * 当前选中的ul下标 + * @type {Number} + * @default 0 + */ + ulIndex: { + type: Number, + default: 0 + } +} +export type CascaderulProps = ExtractPropTypes diff --git a/devui/cascader/components/cascader-list/index.scss b/devui/cascader/components/cascader-list/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..a5ff2f80f31e199faf7c02531b3cbcaebf8d86d7 --- /dev/null +++ b/devui/cascader/components/cascader-list/index.scss @@ -0,0 +1,14 @@ +@import '../../../style/mixins/flex'; +@import '../../../style/theme/color'; + +.devui-cascader-ul { + width: 200px; + height: 180px; + background: $devui-connected-overlay-bg; + display: block; + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + border-left: 1px solid $devui-dividing-line; +} diff --git a/devui/cascader/components/cascader-list/index.tsx b/devui/cascader/components/cascader-list/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4fa19244988f5997d37de3f902b5ab75bf8b2ecf --- /dev/null +++ b/devui/cascader/components/cascader-list/index.tsx @@ -0,0 +1,19 @@ +import { defineComponent } from 'vue' +import { getRootClass } from './use-class' +import './index.scss' +import { cascaderulProps, CascaderulProps } from './cascader-list-types' +import { DCascaderItem } from '../cascader-item' +export default defineComponent({ + name: 'DCascaderList', + props: cascaderulProps, + setup(props: CascaderulProps) { + const rootClasses = getRootClass() + return () => ( + + ) + } +}) diff --git a/devui/cascader/components/cascader-list/use-class.ts b/devui/cascader/components/cascader-list/use-class.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb0109c76ad6d5e3a8e51bf4052efb1a90a93512 --- /dev/null +++ b/devui/cascader/components/cascader-list/use-class.ts @@ -0,0 +1,11 @@ +/** + * 定义组件class + */ + import { computed, ComputedRef } from 'vue'; + + export const getRootClass = (): ComputedRef => { + return computed(() => ({ + 'devui-cascader-ul': true, + })) + } + \ No newline at end of file diff --git a/devui/cascader/hooks/use-cascader-class.ts b/devui/cascader/hooks/use-cascader-class.ts new file mode 100644 index 0000000000000000000000000000000000000000..706eca5edd019b251188470e8bf288d7e421d75f --- /dev/null +++ b/devui/cascader/hooks/use-cascader-class.ts @@ -0,0 +1,16 @@ +/** + * 定义组件class + */ +import { computed, ComputedRef, Ref } from 'vue'; +import { CascaderProps } from '../src/cascader-types' +const TRIGGER_Map = { + hover: 'hover', + click: 'click', +} +export const getRootClass = (props: CascaderProps, menuShow: Ref ): ComputedRef => { + return computed(() => ({ + 'devui-cascader devui-dropdown devui-dropdown-animation': true, + 'devui-dropdown__open': menuShow.value, + 'devui-cascader__disbaled': props.disabled, + })) +} diff --git a/devui/cascader/hooks/use-cascader-options.ts b/devui/cascader/hooks/use-cascader-options.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7a298708b09cd7276035cd0bcd79b08a568608d --- /dev/null +++ b/devui/cascader/hooks/use-cascader-options.ts @@ -0,0 +1,30 @@ +/** + * 处理传入options数据 + */ +import { reactive } from 'vue'; +import { CascaderItem, OptionsCallback } from '../src/cascader-types' +let cascaderOptions +export const optionsHandles = (options?: CascaderItem[]): OptionsCallback => { + if (options) { + cascaderOptions = reactive<[CascaderItem[]]>([ options ]) + } + /** + * hover时修改展示项 + * @param optionItem - 项 + * @param ulIndex - 当前选中的第几级 + * + */ + const changeCascaderIndexs = (optionItem: CascaderItem, ulIndex: number) => { + if (!cascaderOptions) return + if (optionItem?.children?.length > 0) { + cascaderOptions[ulIndex + 1] = optionItem.children + } else { + // 选择的项没有子项时清除多余的ul + cascaderOptions.splice(ulIndex + 1, cascaderOptions.length - 1 - ulIndex) + } + } + return { + cascaderOptions, + changeCascaderIndexs + } +} diff --git a/devui/cascader/hooks/use-cascader-popup.ts b/devui/cascader/hooks/use-cascader-popup.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e8f4bdff2ec8a5c7823c79af0fe426084888bc1 --- /dev/null +++ b/devui/cascader/hooks/use-cascader-popup.ts @@ -0,0 +1,22 @@ +/** + * 控制窗口打开收起 + */ +import { ref, watch } from 'vue'; +import { PopupTypes } from '../src/cascader-types' + +export const popupHandles = (): PopupTypes => { + const menuShow = ref(false) + const menuOpenClass = ref('') + const openPopup = () => { + menuShow.value = !menuShow.value + } + watch(menuShow, (status) => { + menuOpenClass.value = status ? 'devui-drop-menu-wrapper' : '' + }) + + return { + menuShow, + menuOpenClass, + openPopup, + } +} diff --git a/devui/cascader/index.ts b/devui/cascader/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..81e416df6174b05c0b8dd723eb079b8a1e461146 --- /dev/null +++ b/devui/cascader/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import Cascader from './src/cascader' + +Cascader.install = function(app: App): void { + app.component(Cascader.name, Cascader) +} + +export { Cascader } + +export default { + title: 'Cascader 级联菜单', + category: '数据录入', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + + app.use(Cascader as any) + } +} diff --git a/devui/cascader/src/cascader-types.ts b/devui/cascader/src/cascader-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..13abc1af5a1e869e2b9ec394dba7cbef6aa2263f --- /dev/null +++ b/devui/cascader/src/cascader-types.ts @@ -0,0 +1,70 @@ +import type { PropType, ExtractPropTypes, Ref } from 'vue' +import { UnwrapNestedRefs } from '@vue/reactivity' + +type TriggerTypes = 'hover'|'click' + +export interface CascaderItem { + label: string + value: number | string + isLeaf?: boolean + children?: CascaderItem[] + disabled?: boolean + icon?: string + // 用户可以传入自定义属性,并在dropDownItemTemplate中使用 + [prop: string]: any +} + +export const cascaderProps = { + /** + * 可选,指定展开次级菜单方式 + * @description 可选择的值 'hover', 'click' + * @type {('hover'|'click')} + * @default 'hover' + */ + trigger: { + type: String as PropType, + default: 'hover' + }, + /** + * 必选,级联器的菜单信息 + * @type {CascaderItem[]} + * @default [] + */ + options: { + type: Array as PropType, + default: [], + required: true + }, + /** + * 可选,级联器是否禁用 + * @type {boolean} + * @default false + */ + disabled: { + type: Boolean, + default: false + }, + /** + * 可选,没有选择时的输入框展示信息 + * @type {string} + * @default ''' + */ + placeholder: { + type: String, + default: '' + } + +} as const + +export type CascaderProps = ExtractPropTypes + +export interface PopupTypes { + menuShow: Ref + menuOpenClass: Ref + openPopup: (e?: MouseEvent) => void +} + +export interface OptionsCallback { + cascaderOptions: never | UnwrapNestedRefs<[CascaderItem[]]> + changeCascaderIndexs: (optionItem: CascaderItem, ulIndex: number) => void +} \ No newline at end of file diff --git a/devui/cascader/src/cascader.scss b/devui/cascader/src/cascader.scss new file mode 100644 index 0000000000000000000000000000000000000000..cb49189eb72148a52d72dc3eaf1005a43c6a7817 --- /dev/null +++ b/devui/cascader/src/cascader.scss @@ -0,0 +1,71 @@ +@import '../../style/mixins/size'; +@import '../../style/mixins/flex'; +@import '../../style/theme/color'; + +.devui-cascader { + @include flex(flex-start); + + position: relative; + + >div:nth-child(1) { + width: 100%; + } + + &__icon { + position: absolute; + right: 5px; + top: 0; + height: 100%; + @include flex; + @include flex-direction; + + .icon { + margin: 0; + } + } + + input { + width: 100%; + padding-right: 16px; + } + + .devui-drop-menu-wrapper { + display: block; + margin: 4px 0; + font-size: 0; + white-space: nowrap; + padding: 0; + } +} + +.devui-drop-icon-animation { + transition: transform 0.2s linear; +} + +.devui-drop-menu-animation { + transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; + // opacity: 0; + // position: absolute; + // float: left; + z-index: 1000; + // transform: scaleY(0.7) translateY(-5px); + margin-top: 1px; + + .devui-dropdown-menu { + width: auto; + padding-bottom: 0; + @include flex('flex-start'); + } +} + +.devui-dropdown__open { + .devui-cascader__icon { + transform: rotate(180deg); + } + + .devui-drop-menu-animation { + transform-origin: 0 0%; + transform: scaleY(0.9999) translateY(0); + opacity: 1; + } +} diff --git a/devui/cascader/src/cascader.tsx b/devui/cascader/src/cascader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d0565e61ce12839982a0d1a88ec046e2c3617b28 --- /dev/null +++ b/devui/cascader/src/cascader.tsx @@ -0,0 +1,51 @@ +import './cascader.scss' + +import { defineComponent, ref, reactive } from 'vue' +import { cascaderProps, CascaderProps } from './cascader-types' +import { getRootClass } from '../hooks/use-cascader-class' +import { popupHandles } from '../hooks/use-cascader-popup' +import DCascaderList from '../components/cascader-list' +import { optionsHandles } from '../hooks/use-cascader-options' + + +export default defineComponent({ + name: 'DCascader', + props: cascaderProps, + setup(props: CascaderProps, ctx) { + const origin = ref(null) + const position = reactive({ + originX: 'left', + originY: 'bottom', + overlayX: 'left', + overlayY: 'top' + } as const) + // popup弹出层 + const { menuShow, menuOpenClass, openPopup } = popupHandles() + // 配置class + const rootClasses = getRootClass(props, menuShow) + // 级联菜单操作,变换ul、li等 + const { cascaderOptions } = optionsHandles(props.options) + return () => ( + <> +
    + +
    + +
    +
    + +
    +
    + {cascaderOptions.map((item, index) => { + return + })} +
    +
    +
    + + ) + }, +}) diff --git a/docs/components/cascader/index.md b/docs/components/cascader/index.md new file mode 100644 index 0000000000000000000000000000000000000000..88e98169a7e3e0f676a2c72975db7cad7db6ec89 --- /dev/null +++ b/docs/components/cascader/index.md @@ -0,0 +1,145 @@ +# 级联菜单 +下拉级联菜单。 + +### 基本用法 +:::demo + +```vue + + +``` + +::: + +### API + +| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置项 | +| :--------: | :------: | :-------: | :---------------------- | --------------------------------- | --------- | +| options | [`CascaderItem[]`](#CascaderItem) | [] | 必选,级联器的菜单信息 | [基本用法](#基本用法) | | +| placeholder | `string` | '' | 可选,没有选择时的输入框展示信息 | [基本用法](#基本用法) | | + +### 接口 & 类型定义 + +- + +#### CascaderItem +```ts +interface CascaderItem { + label: string; + value: number | string; + isLeaf?: boolean; + children?: CascaderItem[]; + disabled?: boolean; + icon?: string; + // 用户可以传入自定义属性,并在dropDownItemTemplate中使用 + [prop: string]: any; +} +``` \ No newline at end of file