diff --git a/packages/devui-vue/devui-cli/commands/generate-dts.js b/packages/devui-vue/devui-cli/commands/generate-dts.js new file mode 100644 index 0000000000000000000000000000000000000000..96e820e089ceb4796327cd17bee41b7073c2d67d --- /dev/null +++ b/packages/devui-vue/devui-cli/commands/generate-dts.js @@ -0,0 +1,38 @@ +const path = require('path') +const fs = require('fs') +const fse = require('fs-extra') +const logger = require('../shared/logger') + +const entryDir = path.resolve(__dirname, '../../devui') +const outputDir = path.resolve(__dirname, '../../build') + +function generateIndexDts(buildDir) { + const fileStr = `import { App } from 'vue'; + declare function install(app: App): void + declare const _default: { + install: typeof install; + version: string; + }; + export default _default;` + fse.outputFileSync(path.resolve(buildDir, 'index.d.ts'), fileStr, 'utf8') +} + +exports.generateDts = () => { + generateIndexDts(outputDir) + + const components = fs.readdirSync(entryDir).filter(name => { + const componentDir = path.resolve(entryDir, name) + const isDir = fs.lstatSync(componentDir).isDirectory() + return isDir && fs.readdirSync(componentDir).includes('index.ts') + }) + const srcDts = path.resolve(outputDir, 'index.d.ts') + + for(const name of components) { + const destDts = path.resolve(outputDir, `${name}/index.d.ts`) + fs.copyFile(srcDts, destDts, (err) => { + if (err) { + logger.error(`拷贝组件${name}的ts类型文件失败!`) + } + }) + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui-cli/index.js b/packages/devui-vue/devui-cli/index.js index d4ed9906e6fe7e2f2b8c41cab1765391bdae6fd0..2e714064bb73c0dd23a323287413648d7d40c323 100755 --- a/packages/devui-vue/devui-cli/index.js +++ b/packages/devui-vue/devui-cli/index.js @@ -3,6 +3,7 @@ const { Command } = require('commander') const { create, validateCreateType } = require('./commands/create') const { build } = require('./commands/build') const { generateTheme } = require('./commands/generate-theme') +const { generateDts } = require('./commands/generate-dts') const { VERSION, CREATE_SUPPORT_TYPES } = require('./shared/constant') const program = new Command() @@ -18,6 +19,8 @@ program program .command('build') .description('打包组件库') + .hook('postAction', generateTheme) + .hook('postAction', generateDts) .action(build) program @@ -25,4 +28,9 @@ program .description('生成主题变量文件') .action(generateTheme) +program + .command('generate:dts') + .description('生成ts类型文件') + .action(generateDts) + program.parse().version(VERSION) diff --git a/packages/devui-vue/devui/accordion/src/accordion-list.tsx b/packages/devui-vue/devui/accordion/src/accordion-list.tsx index 362cae3e585c1af33208f82104f3fe57a8903fb7..3fba7abb5264b37250d2cb0f92588fe9256af854 100644 --- a/packages/devui-vue/devui/accordion/src/accordion-list.tsx +++ b/packages/devui-vue/devui/accordion/src/accordion-list.tsx @@ -1,11 +1,17 @@ -import { defineComponent } from 'vue' -import { AccordionMenuItem } from './accordion.type' +import { defineComponent, toRefs } from 'vue' +import type { AccordionMenuItem } from './accordion.type' +import DAccordionMenu from './accordion-menu' +import { accordionProps } from './accordion-types' + export default defineComponent({ name: 'DAccordionList', inheritAttrs: false, + components: { + DAccordionMenu + }, props: { - data: { + data: { type: Array as () => Array, default: null }, @@ -13,47 +19,66 @@ export default defineComponent({ type: Number, default: 0 }, - parent: { + parent: { type: Object as () => AccordionMenuItem, default: null }, innerListTemplate: Boolean, + ...accordionProps, }, - setup(props, ctx) { + setup(props, {attrs, slots}) { + const { + childrenKey, + innerListTemplate, + deepth + } = toRefs(props) return () => { - return (!props.innerListTemplate || props.deepth === 0) && + } + + ) } } }) \ No newline at end of file diff --git a/packages/devui-vue/devui/accordion/src/accordion-menu.tsx b/packages/devui-vue/devui/accordion/src/accordion-menu.tsx index c5280a3b4d5a3355d8f445327de69a98828020db..e79c63d04917e6815844b6c52732f445de2765f9 100644 --- a/packages/devui-vue/devui/accordion/src/accordion-menu.tsx +++ b/packages/devui-vue/devui/accordion/src/accordion-menu.tsx @@ -1,13 +1,58 @@ -import { defineComponent } from 'vue' +import { computed, defineComponent, toRefs } from 'vue' +import { AccordionMenuItem } from './accordion.type' +import DAccordionList from './accordion-list' +import { accordionProps } from './accordion-types' export default defineComponent({ name: 'DAccordionMenu', props: { - + item: Object as () => AccordionMenuItem, + deepth: { + type: Number, + default: 0 + }, + parent: { + type: Object as () => AccordionMenuItem, + default: null + }, + ...accordionProps }, - setup() { - return () => { - return
  • d-accordion-menu
  • + setup(props) { + const { item, deepth } = toRefs(props) + + + const menuItemClasses = computed(() => { + return (keyOpen === undefined && props.autoOpenActiveMenu) + ? childActived + : keyOpen + }) + + const keyOpen = computed(() => { + return item?.value[props.openKey]; + }) + const childActived = computed(() => { + // return props.routerLinkActived || props.hasActiveChildren + }) + + return () => { + return ( + <> +
    + {item.value.title} +
    + + + + ) } } }) \ No newline at end of file diff --git a/packages/devui-vue/devui/accordion/src/accordion-types.ts b/packages/devui-vue/devui/accordion/src/accordion-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..b64c04456f8755171c367044f3fccae6035e7664 --- /dev/null +++ b/packages/devui-vue/devui/accordion/src/accordion-types.ts @@ -0,0 +1,63 @@ +import { ExtractPropTypes } from "vue"; +import { AccordionMenuType } from "./accordion.type"; + +export const accordionProps = { + data: { + type: Array as () => Array | AccordionMenuType, + default: null, + }, + /* Key值定义, 用于自定义数据结构 */ + titleKey: { type: String, default: "title" }, // 标题的key,item[titleKey]类型为string,为标题显示内容 + loadingKey: { type: String, default: "loading" }, // 子菜单动态加载item[loadingKey]类型为boolean + childrenKey: { type: String, default: "children" }, // 子菜单Key + disabledKey: { type: String, default: "disabled" }, // 是否禁用Key + activeKey: { type: String, default: "active" }, // 菜单是否激活/选中 + openKey: { type: String, default: "open" }, // 菜单是否打开 + + /* 菜单模板 */ + menuItemTemplate: { type: String, default: "" }, // 可展开菜单内容条模板 + itemTemplate: { type: String, default: "" }, // 可点击菜单内容条模板 + + menuToggle: { + type: Function as unknown as () => (event: MouseEvent) => void, + default: null, + }, // 可展开菜单展开事件 + itemClick: { + type: Function as unknown as () => (event: MouseEvent) => void, + default: null, + }, // 可点击菜单点击事件 + activeItemChange: { + type: Function as unknown as () => (event: MouseEvent) => void, + default: null, + }, + + /** 高级选项和模板 */ + restrictOneOpen: { type: Boolean, default: false }, // 限制一级菜单同时只能打开一个 + autoOpenActiveMenu: { type: Boolean, default: false }, // 自动展开活跃菜单 + showNoContent: { type: Boolean, default: true }, // 没有内容的时候是否显示没有数据 + noContentTemplate: { type: String, default: "" }, // 没有内容的时候使用自定义模板 + loadingTemplate: { type: String, default: "" }, // 加载中使用自定义模板 + innerListTemplate: { type: String, default: "" }, // 可折叠菜单内容完全自定义,用做折叠面板 + + /* 内置路由/链接/动态判断路由或链接类型 */ + linkType: { + type: String as () => + | "routerLink" + | "hrefLink" + | "dependOnLinkTypeKey" + | "" + | string, + default: "", + }, + linkTypeKey: { type: String, default: "linkType" }, // linkType为'dependOnLinkTypeKey'时指定对象linkType定义区 + linkKey: { type: String, default: "link" }, // 链接内容的key + linkTargetKey: { type: String, default: "target" }, // 链接目标窗口的key + linkDefaultTarget: { type: String, default: "_self" }, // 不设置target的时候target默认值 + + accordionType: { + type: String as () => "normal" | "embed", + default: "normal", + }, +} as const; + +export type AccordionProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/accordion/src/accordion.scss b/packages/devui-vue/devui/accordion/src/accordion.scss index a6f4317d37ece561d874963ff04c5c8bbca90256..6aa628b208af4ed2382bd33b8f0c76214c6878b1 100644 --- a/packages/devui-vue/devui/accordion/src/accordion.scss +++ b/packages/devui-vue/devui/accordion/src/accordion.scss @@ -3,6 +3,7 @@ @import '../../style/theme/font'; @import '../../style/theme/shadow'; @import '../../style/theme/corner'; +@import '../../style/core/animation'; :host { display: block; @@ -188,17 +189,40 @@ d-accordion-item-routerlink { } } + .devui-accordion-splitter::before { + content: ''; + display: block; + width: 2px; + height: 18px; + background: $devui-form-control-line-active; + position: absolute; + top: 11px; + left: 0; + opacity: 0; + } + &.devui-router-active, &.active { &:not(.open) .devui-accordion-splitter::before { - content: ''; - display: block; - width: 2px; - height: 18px; - background: $devui-form-control-line-active; - position: absolute; - top: 11px; - left: 0; + opacity: 1; + } + } +} + +.devui-accordion-show-animate .devui-accordion-item-title { + transition: + font-weight $devui-animation-duration-fast $devui-animation-ease-in-out-smooth, + background-color $devui-animation-duration-fast $devui-animation-ease-in-out-smooth; + + .devui-accordion-splitter::before { + transform: scaleY(0); + transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth; + } + + &.devui-router-active, + &.active { + &:not(.open) .devui-accordion-splitter::before { + transform: scaleY(1); } } } diff --git a/packages/devui-vue/devui/accordion/src/accordion.tsx b/packages/devui-vue/devui/accordion/src/accordion.tsx index 9935f71d21e52aa811e6db1ac5dafa6284bdbbd8..f3cb838a9c7a0131c5b5509ce399e232017ddd61 100644 --- a/packages/devui-vue/devui/accordion/src/accordion.tsx +++ b/packages/devui-vue/devui/accordion/src/accordion.tsx @@ -1,65 +1,106 @@ -import { defineComponent, reactive } from 'vue' +import { defineComponent, onBeforeUpdate, onMounted, ref, SetupContext, toRefs, watch } from 'vue' import AccordionList from './accordion-list' -import { AccordionMenuType } from './accordion.type' +import { accordionProps, AccordionProps } from './accordion-types' +import { AccordionItemClickEvent, AccordionMenuItem, AccordionMenuToggleEvent } from './accordion.type' import './accordion.scss' export default defineComponent({ name: 'DAccordion', - props: { - data: { - type: Array as () => Array | AccordionMenuType, - default: null - }, - /* Key值定义, 用于自定义数据结构 */ - titleKey: { type : String, default: 'title' }, // 标题的key,item[titleKey]类型为string,为标题显示内容 - loadingKey: { type : String, default: 'loading' }, // 子菜单动态加载item[loadingKey]类型为boolean - childrenKey: { type : String, default: 'children' }, // 子菜单Key - disabledKey: { type : String, default: 'disabled' }, // 是否禁用Key - activeKey: { type : String, default: 'active' }, // 菜单是否激活/选中 - openKey: { type : String, default: 'open' }, // 菜单是否打开 + props: accordionProps, + setup(props: AccordionProps, { emit }) { + const { data, childrenKey, activeKey, openKey ,accordionType, autoOpenActiveMenu , restrictOneOpen} = toRefs(props) - /* 菜单模板 */ - menuItemTemplate: { type: String, default: '' }, // 可展开菜单内容条模板 - itemTemplate: { type: String, default: '' }, // 可点击菜单内容条模板 + let clickActiveItem: AccordionMenuItem | undefined = undefined //记录用户点击的激活菜单项 - menuToggle: { - type: Function as unknown as () => ((event: MouseEvent) => void), - default: null - }, // 可展开菜单展开事件 - itemClick: { - type: Function as unknown as () => ((event: MouseEvent) => void), - default: null - }, // 可点击菜单点击事件 - activeItemChange: { - type: Function as unknown as () => ((event: MouseEvent) => void), - default: null - }, + const flatten = (arr: Array, childrenKey = 'children', includeParent = false, includeLeaf = true) => { + return arr.reduce((acc, cur) => { + const children = cur[childrenKey]; + if (children === undefined) { + if (includeLeaf) { + acc.push(cur); + } + } else { + if (includeParent) { + acc.push(cur); + } + if (Array.isArray(children)) { + acc.push(...flatten(children, childrenKey, includeParent)); + } + } + return acc; + }, []); + } + + const initActiveItem = () => { + const activeItem = flatten(data.value, childrenKey.value) + .filter(item => item[activeKey.value]).pop(); + if (activeItem) { + if (!clickActiveItem) { + activeItemFn(activeItem); + } + } else { + clickActiveItem = undefined; + } + } + + // 激活子菜单项并去掉其他子菜单的激活 + const activeItemFn = (item) => { + if (clickActiveItem && clickActiveItem[activeKey.value]) { + clickActiveItem[activeKey.value] = false; + } + item[activeKey.value] = true; + clickActiveItem = item; + emit('activeItemChange', clickActiveItem) + } + // 打开或关闭一级菜单,如果有限制只能展开一项则关闭其他一级菜单 + const openMenuFn = (item, open) => { + if (open && restrictOneOpen.value) { + data.value.forEach(itemtemp => { itemtemp[openKey.value] = false; }); + } + item[openKey.value] = open; + } - /** 高级选项和模板 */ - restrictOneOpen: { type: Boolean, default: false }, // 限制一级菜单同时只能打开一个 - autoOpenActiveMenu: { type: Boolean, default: false }, // 自动展开活跃菜单 - showNoContent: { type: Boolean, default: true }, // 没有内容的时候是否显示没有数据 - noContentTemplate: { type: String, default: '' }, // 没有内容的时候使用自定义模板 - loadingTemplate: { type: String, default: '' }, // 加载中使用自定义模板 - innerListTemplate: { type: String, default: '' }, // 可折叠菜单内容完全自定义,用做折叠面板 + // 点击了可点击菜单 + const itemClickFn = (itemEvent: AccordionItemClickEvent) => { + const prevActiveItem = clickActiveItem; + activeItemFn(itemEvent.item); + emit('itemClick', {...itemEvent, prevActiveItem: prevActiveItem}); + } - /* 内置路由/链接/动态判断路由或链接类型 */ - linkType: { - type: String as () => 'routerLink' | 'hrefLink' | 'dependOnLinkTypeKey' | '' | string, - default: '' - }, - linkTypeKey: { type: String, default: 'linkType' }, // linkType为'dependOnLinkTypeKey'时指定对象linkType定义区 - linkKey: { type: String, default: 'link' }, // 链接内容的key - linkTargetKey: { type: String, default: 'target' }, // 链接目标窗口的key - linkDefaultTarget: { type: String, default: '_self' }, // 不设置target的时候target默认值 + const linkItemClickFn = (itemEvent: AccordionItemClickEvent) => { + const prevActiveItem = clickActiveItem; + clickActiveItem = itemEvent.item; + emit('itemClick', {...itemEvent, prevActiveItem: prevActiveItem}); + } + + // 打开或关闭可折叠菜单 + const menuToggleFn = (menuEvent: AccordionMenuToggleEvent) => { + openMenuFn(menuEvent.item, menuEvent.open); + emit('menuToggle', menuEvent); + } + + const cleanOpenData = () => { + flatten(data.value, childrenKey.value, true, false).forEach( + item => item[openKey.value] = undefined + ) + } - accordionType: { type: String as () => 'normal' | 'embed', default: 'normal' }, - }, - setup(props) { - const { data, accordionType } = reactive(props) + onMounted(() => { + if (data.value) { + initActiveItem(); + } + }) + + watch(() => autoOpenActiveMenu.value, (current, preV) => { + console.log('cur, new', current, preV) + if (current && preV === false) { + cleanOpenData(); + } + }) + return () => { - return
    + return
    { - const { cascaderli, ulIndex } = props - const { changeCascaderIndexs } = optionsHandles() - const rootClasses = getRootClass() - const mouseHover = () => { - changeCascaderIndexs(cascaderli, ulIndex) +export const DCascaderItem = (props: CascaderItemPropsType) => { + // console.log('item index',props) + const { cascaderItem, ulIndex, liIndex, cascaderItemNeedProps, cascaderOptions } = props + const { multiple, stopDefault, valueCache, activeIndexs, trigger, confirmInputValueFlg, tagList} = cascaderItemNeedProps + const isTriggerHover = trigger === 'hover' + const rootClasses = useListClassName(props) + const { updateStatus } = updateCheckOptionStatus(tagList) + const disbaled = computed(() => cascaderItem?.disabled) // 当前项是否被禁用 + // 触发联动更新 + const updateValues = () => { + if (stopDefault.value) return + // 删除当前联动级之后的所有级 + activeIndexs.splice(ulIndex, activeIndexs.length - ulIndex) + // 更新当前渲染视图的下标数组 + activeIndexs[ulIndex] = liIndex + if (!multiple) { // 单选点击选项就更新,多选是通过点击checkbox触发数据更新 + singleChoose(ulIndex, valueCache, cascaderItem) + } + } + // 鼠标hover(多选模式下只能点击操作触发) + const mouseEnter = () => { + if (disbaled.value || multiple) return + updateValues() + } + const mouseenter = { + [ isTriggerHover && 'onMouseenter' ]: mouseEnter + } + // 鼠标click + const mouseClick = () => { + if (disbaled.value) return + updateValues() + if (!multiple && (!cascaderItem.children || cascaderItem?.children?.length === 0)) { + confirmInputValueFlg.value = !confirmInputValueFlg.value + } + } + const checkboxChange = () => { + updateStatus(cascaderItem, cascaderOptions, ulIndex) } return ( -
  • - - { - cascaderli?.children?.length > 0 && +
  • + { multiple && +
    + +
    } +
    + { cascaderItem.icon && +
    + +
    + } + + { + cascaderItem?.children?.length > 0 && + } +
  • ) } diff --git a/packages/devui-vue/devui/cascader/components/cascader-item/use-class.ts b/packages/devui-vue/devui/cascader/components/cascader-item/use-class.ts deleted file mode 100644 index 5a5ec3e21b6bd16e8fafc7da41ea55a592f0760b..0000000000000000000000000000000000000000 --- a/packages/devui-vue/devui/cascader/components/cascader-item/use-class.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 定义组件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/packages/devui-vue/devui/cascader/components/cascader-list/cascader-list-types.ts b/packages/devui-vue/devui/cascader/components/cascader-list/cascader-list-types.ts deleted file mode 100644 index 85e2152df95d7ff7843a6db28e084074b8c38f24..0000000000000000000000000000000000000000 --- a/packages/devui-vue/devui/cascader/components/cascader-list/cascader-list-types.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/packages/devui-vue/devui/cascader/components/cascader-list/index.scss b/packages/devui-vue/devui/cascader/components/cascader-list/index.scss index a5ff2f80f31e199faf7c02531b3cbcaebf8d86d7..76a260c86f4545d6b1bb5a1ce004d9d54b7a777a 100644 --- a/packages/devui-vue/devui/cascader/components/cascader-list/index.scss +++ b/packages/devui-vue/devui/cascader/components/cascader-list/index.scss @@ -1,8 +1,8 @@ @import '../../../style/mixins/flex'; @import '../../../style/theme/color'; +@import '../../../style/theme/font'; .devui-cascader-ul { - width: 200px; height: 180px; background: $devui-connected-overlay-bg; display: block; @@ -11,4 +11,17 @@ padding: 0; overflow-y: auto; border-left: 1px solid $devui-dividing-line; + + &.devui-drop-no-data { + color: $devui-disabled-text; + padding: 8px; + display: block; + height: 36px; + line-height: 36px; + overflow-y: hidden; + background-color: $devui-disabled-bg; + font-size: $devui-font-size; + @include flex(center, flex-start); + @include flex-direction(); + } } diff --git a/packages/devui-vue/devui/cascader/components/cascader-list/index.tsx b/packages/devui-vue/devui/cascader/components/cascader-list/index.tsx index 4fa19244988f5997d37de3f902b5ab75bf8b2ecf..fb367f22f1c0043928da413d7252bb6112dde7da 100644 --- a/packages/devui-vue/devui/cascader/components/cascader-list/index.tsx +++ b/packages/devui-vue/devui/cascader/components/cascader-list/index.tsx @@ -1,18 +1,26 @@ import { defineComponent } from 'vue' -import { getRootClass } from './use-class' -import './index.scss' -import { cascaderulProps, CascaderulProps } from './cascader-list-types' +import { useUlClassName } from '../../hooks/use-cascader-class' +import { useDropdownStyle } from '../../hooks/use-cascader-style' +import { cascaderulProps, CascaderulProps } from '../../src/cascader-types' + import { DCascaderItem } from '../cascader-item' +import './index.scss' export default defineComponent({ name: 'DCascaderList', props: cascaderulProps, setup(props: CascaderulProps) { - const rootClasses = getRootClass() + const ulClass = useUlClassName(props) + const ulStyle = useDropdownStyle(props) + // console.log('props', props) return () => ( -
      - {props.cascaderlis.map((item, index) => { - return - })} +
        + { + props?.cascaderItems?.length > 0 + ? props.cascaderItems.map((item, index) => { + return + }) + : 没有数据 + }
      ) } diff --git a/packages/devui-vue/devui/cascader/components/cascader-list/use-class.ts b/packages/devui-vue/devui/cascader/components/cascader-list/use-class.ts deleted file mode 100644 index eb0109c76ad6d5e3a8e51bf4052efb1a90a93512..0000000000000000000000000000000000000000 --- a/packages/devui-vue/devui/cascader/components/cascader-list/use-class.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 定义组件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/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..848f54b3c6c28fc674ed812bcc8bc26c9723f6e4 --- /dev/null +++ b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.scss @@ -0,0 +1,29 @@ +@import '../../../style/theme/color'; +@import '../../../style/theme/corner'; +@import '../../../style/core/font'; +.devui-tags { + &-input { + flex: 1; + padding: 1px 20px 1px 4px; + border: 1px solid $devui-form-control-line; + border-radius: $devui-border-radius; + outline: none; + background-color: $devui-base-bg; + transition: border-color 300ms cubic-bezier(0.645, 0.045, 0.355, 1); + } + &-box { + width: 100%; + overflow: auto; + min-height: 28px; + max-height: 56px; + display: flex; + align-items: center; + flex-wrap: wrap; + } + &-placeholder { + font-size: $devui-font-size; + line-height: 22px; + margin-left: 6px; + color: $devui-placeholder; + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/components/cascader-multiple/index.tsx b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..932c4a3f3bac097c584af244187ab34d47c93704 --- /dev/null +++ b/packages/devui-vue/devui/cascader/components/cascader-multiple/index.tsx @@ -0,0 +1,20 @@ +/** + * 多选模式下的内容框 + */ +import DTag from '../cascader-tag/' +import { MultiplePropsType } from '../../src/cascader-types' +import './index.scss' +export default (props: MultiplePropsType) => { + return ( +
      +
      + { props.activeOptions.length > 0 + ? props.activeOptions.map(item => { + return + }) + :
      { props.placeholder }
      + } +
      +
      + ) +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss b/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..2e57f9bc2fd1f26e139bc29ccf575fa55866f105 --- /dev/null +++ b/packages/devui-vue/devui/cascader/components/cascader-tag/index.scss @@ -0,0 +1,39 @@ +@import '../../../style/theme/color'; +@import '../../../style/theme/corner'; +@import '../../../style/core/font'; +.devui-tag { + margin: 2px 4px 2px 0; + display: inline-block; + position: relative; + display: flex; + align-items: center; + padding: 0 8px 0 8px; + background-color: $devui-label-bg; + border-radius: $devui-border-radius; + border-color: inherit; + border: 0 solid; + span { + min-height: 20px; + line-height: 20px; + font-size: $devui-font-size; + color: $devui-text; + position: relative; + cursor: default; + } + &__close { + margin-left: 12px; + font-size: $devui-font-size; + cursor: pointer; + color: #fff; + width: 14px; + height: 14px; + line-height: 14px; + background-color: $devui-line; + border-radius: 50%; + display: inline-block; + text-align: center; + &:hover { + background-color: $devui-brand; + } + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/components/cascader-tag/index.tsx b/packages/devui-vue/devui/cascader/components/cascader-tag/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8c4ff034af70e4eae0b492f26eaac911799f23e3 --- /dev/null +++ b/packages/devui-vue/devui/cascader/components/cascader-tag/index.tsx @@ -0,0 +1,26 @@ +/** + * 多选模式下的内容框中的选中tag + * tag组件还未开发完成,所以暂时使用自定义组件 + */ +import { CascaderItem } from '../../src/cascader-types' +import { multipleDeleteTag } from '../../hooks/use-cascader-multiple' +import './index.scss' +interface PropsType { + tag: CascaderItem + tagList: CascaderItem[] +} +export default (props: PropsType) => { + const { tagList, tag } = props + const deleteCurrentTag = (e: Event) => { + e.stopPropagation() + multipleDeleteTag(tagList, tag) + } + return ( +
      + { tag?.label } +
      + +
      +
      + ) +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-class.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-class.ts index 706eca5edd019b251188470e8bf288d7e421d75f..576dc61c016d1fc694ecae6da945ca815bb8a5c4 100644 --- a/packages/devui-vue/devui/cascader/hooks/use-cascader-class.ts +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-class.ts @@ -2,15 +2,39 @@ * 定义组件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 => { +import { CascaderProps, CascaderulProps, CascaderItemPropsType } from '../src/cascader-types' +// import { UseClassNameType } from '../components/cascader-item/cascader-item-types' + +// 根节点class +export const useRootClassName = (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, })) } + +// 弹出层项 class +export const useListClassName = (props: CascaderItemPropsType): ComputedRef => { + const itemProps = props?.cascaderItemNeedProps + const isActive = itemProps?.valueCache[props.ulIndex] === props.cascaderItem?.value + const isDisabled = props.cascaderItem?.disabled + return computed(() => ({ + 'devui-cascader-li devui-dropdown-item': true, + 'devui-leaf-active': isActive, + 'disabled': isDisabled + })) +} + +// 弹出层列 class +export const useUlClassName = (props: CascaderulProps): ComputedRef => { + return computed(() => ({ + 'devui-cascader-ul': true, + 'devui-drop-no-data': props?.cascaderItems?.length === 0 + })) +} + +// 为弹出层打开添加全局class +export const dropdownOpenClass = (status: boolean): string => { + return status ? 'devui-drop-menu-wrapper' : '' +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts new file mode 100644 index 0000000000000000000000000000000000000000..603d7b4a09d84eae7c063b079186e591f9eed2dc --- /dev/null +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-item.ts @@ -0,0 +1,28 @@ +/** + * 处理cascader-item中需要的参数 + */ +import { cloneDeep } from 'lodash-es' +import { ref, reactive, Ref } from 'vue' +import { CascaderProps, UseCascaderItemCallback, CascaderItem } from '../src/cascader-types' + +export const useCascaderItem = (props?: CascaderProps, stopDefault?: Ref, tagList?: CascaderItem[]): UseCascaderItemCallback => { + /** + * 传递给cascader-item/index.ts组件的数据 + */ + const cascaderItemNeedProps = { + trigger: props.trigger, + inputValueCache: ref(''), + confirmInputValueFlg: ref(false), // 用于监听点击确定时输出选择内容 + valueCache: reactive(cloneDeep(props.value)), // 操作时缓存选中的值 + value: reactive(cloneDeep(props.value)), // 每级的value + multiple: props.multiple, + activeIndexs: reactive([]), // 维护用于视图更新的选中下标 + tagList, // 多选模式下选中的值数组,用于生成tag + stopDefault, + } + + return { + cascaderItemNeedProps, + // getInputValue + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-multiple.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-multiple.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a2caaf4f7401695dff61a1871cc77b84f50e210 --- /dev/null +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-multiple.ts @@ -0,0 +1,191 @@ +/** + * 多选模式 + */ +import { CascaderItem, UpdateStatusCallback, CaascaderOptionsType, CheckedType, CascaderItemNeedType } from '../src/cascader-types' + +/** + * 初始化选中项,将选中的数组集合置为空 + * @param tagList 前被选中的tagList集合 + */ +export const initTagList = (tagList: CascaderItem[]): void => { + tagList.splice(0, tagList.length) +} +/** + * 添加选中项 + * @param tagList 当前被选中的tagList集合 + * @param singleItem 当前选中项 + * + */ +export const multipleAddTag = (tagList: CascaderItem[], singleItem: CascaderItem): void => { + tagList.push(singleItem) +} + +/** + * 删除选中项 + * @param tagList 前被选中的tagList集合 + * @param singleItem 当前选中项 + * + */ +export const multipleDeleteTag = (tagList: CascaderItem[], singleItem: CascaderItem): void => { + // console.log(arr) + const i = tagList.findIndex(item => item.value === singleItem.value) + tagList.splice(i, 1) +} + +/** + * 多选模式初始化选中的节点 + * @param targetValues 多选模式下的value数组 + * @param rootNode 选项的第一列 + * @param tagList 选中的tag集合 + */ +export const initMultipleCascaderItem = (targetValues: number[], rootColumn: CascaderItem[], tagList: CascaderItem[]): void => { + findNextColumn(targetValues, rootColumn, 0, tagList) +} + +/** + * 根据values集合递归获取选中的节点 + * @param targetValues 多选模式下的value数组 + * @param rootNode 选项的第一列 + * @param index 当前初始化的列下标 + * @param tagList 选中的tag集合 + */ +const findNextColumn = (targetValues: number[], options: CascaderItem[], index: number, tagList: CascaderItem[]): void => { + let targetNode = options.find(t => t.value === targetValues[index]) // 根据value获取当前选中的项 + if (targetNode?.children?.length > 0) { // 递归的限制条件,是否还有子级 + index += 1 // 进入下一级 + targetNode = setChildrenParent(targetNode) // 为children设置parent,方便后续通过child使用parent + findNextColumn(targetValues, targetNode.children, index, tagList) + } else { // 没有子节点说明此时已经是最终结点了 + multipleAddTag(tagList, targetNode) // 新增tag + + targetNode['checked'] = true + + // 从最终结点往上寻找父节点更新状态 + // 通过父亲节点查询所有子节点状态从而更新父节点状态 + findChildrenCheckedStatusToUpdateParent(targetNode?.parent) + } +} + /** + * + * @param parentNode 父节点 + * @returns parentNode 父节点 + */ + const setChildrenParent = (parentNode) => { + parentNode?.children.forEach(child => { + child.parent = parentNode + }) + return parentNode + } + +export const updateCheckOptionStatus = (tagList: CascaderItem[]): UpdateStatusCallback => { + /** + * 更新当前选中状态 + * @param node 当前结点 + * @param options column的集合 + * @param ulIndex 当前column的下标 + */ + const updateStatus = (node: CascaderItem, options: CaascaderOptionsType, ulIndex: number) => { + // 更新当前点击的node + updateCurNodeStatus(node, ulIndex) + ulIndex -= 1 + // const parentNode = getParentNode(node.value, options, ulIndex) + const parentNode = node?.parent + updateParentNodeStatus(parentNode, options, ulIndex) + } + /** + * 更新当前选中的结点状态 + * @param node 当前结点 + * @param ulIndex 当前column的下标 + */ + const updateCurNodeStatus = (node: CascaderItem, ulIndex: number) => { + // 如果是半选状态,更新为false,其他状态则更新为与checked相反 + if (node?.halfChecked) { // 更新半选状态 + node['halfChecked'] = false + node['checked'] = false + updateCheckStatusLoop(node, 'halfChecked', ulIndex) + } else { + node['checked'] = !node.checked + // 更新是否选中状态 + updateCheckStatusLoop(node, 'checked', ulIndex, node.checked) + } + } + /** + * 父节点改变子节点check状态 + * @param node 节点 + */ + const updateCheckStatusLoop = (node: CascaderItem, type: CheckedType, ulIndex: number, status?: boolean) => { + if (node?.children?.length > 0) { + node.children.forEach(item => { + // 当需要改变checked时 + // halfChecked一定是false + if (item.disabled) return // 禁用不可更改状态 + if (type === 'checked') { + item[type] = status + item['halfChecked'] = false + updateCheckStatusLoop(item, type, ulIndex, status) + } else if (type === 'halfChecked') { + /** + * halfChecked为false时,取消子节点所有选中 + */ + item['halfChecked'] = false + item['checked'] = false + !status && updateCheckStatusLoop(item, type, ulIndex) + } + }) + } else { + // 增加或者删除选中的项 + !node.checked + ? multipleDeleteTag(tagList, node) + : multipleAddTag(tagList, node) + } + } + return { + updateStatus, + } +} +/** + * 子节点获取父节点 + * 已在子节点创建父节点,此段代码不再使用 + */ +// const getParentNode = (childValue: string | number, options: CaascaderOptionsType, ulIndex: number): CascaderItem => { +// if (ulIndex < 0) return +// const queue = [...options[ulIndex]] +// let cur: CascaderItem +// while(queue.length) { +// cur = queue.shift() +// if (cur.children && cur.children.find(t => t.value === childValue)) { +// break +// } else if (cur.children) { +// queue.push(...cur.children) +// } +// } +// return cur +// } + +/** + * 根据当前节点的子节点更新当前节点状态 + * @param node - 当前节点 + */ + const findChildrenCheckedStatusToUpdateParent = (node) => { + const checkedChild = node?.children?.find(t => t['checked']) + const halfcheckedChild = node?.children?.find(t => t['halfChecked']) + const uncheckedChild = node?.children?.find(t => !t['halfChecked'] && !t['checked']) + if (halfcheckedChild || (checkedChild && uncheckedChild)) { + node['checked'] = false + node['halfChecked'] = true + } else if (!checkedChild && !halfcheckedChild) { + node['checked'] = false + node['halfChecked'] = false + } else { + node['checked'] = true + node['halfChecked'] = false + } +} +const updateParentNodeStatus = (node: CascaderItem, options: CaascaderOptionsType, ulIndex: number) => { + if (ulIndex < 0) return + findChildrenCheckedStatusToUpdateParent(node) + ulIndex -= 1 + // const parentNode = getParentNode(node.value, options, ulIndex) + const parentNode = node?.parent + updateParentNodeStatus(parentNode, options, ulIndex) +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-options.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-options.ts index e7a298708b09cd7276035cd0bcd79b08a568608d..1c3c897aa001f90d7b0022beb155fccd9c2124bd 100644 --- a/packages/devui-vue/devui/cascader/hooks/use-cascader-options.ts +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-options.ts @@ -1,15 +1,11 @@ /** * 处理传入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 ]) - } +import { CascaderItem, OptionsCallback, CaascaderOptionsType } from '../src/cascader-types' +export const optionsHandles = (cascaderOptions?: CaascaderOptionsType): OptionsCallback => { + /** - * hover时修改展示项 + * change时修改展示项 * @param optionItem - 项 * @param ulIndex - 当前选中的第几级 * @@ -23,6 +19,7 @@ export const optionsHandles = (options?: CascaderItem[]): OptionsCallback => { cascaderOptions.splice(ulIndex + 1, cascaderOptions.length - 1 - ulIndex) } } + return { cascaderOptions, changeCascaderIndexs diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-popup.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-popup.ts index 8e8f4bdff2ec8a5c7823c79af0fe426084888bc1..2f4bb5f1ad1e3518a8e504a5290b89adb6072b81 100644 --- a/packages/devui-vue/devui/cascader/hooks/use-cascader-popup.ts +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-popup.ts @@ -1,22 +1,33 @@ /** * 控制窗口打开收起 */ -import { ref, watch } from 'vue'; -import { PopupTypes } from '../src/cascader-types' - -export const popupHandles = (): PopupTypes => { +import { ref, watch, computed } from 'vue'; +import { PopupTypes, CascaderProps } from '../src/cascader-types' +import { dropdownOpenClass } from './use-cascader-class' +export const popupHandles = (props: CascaderProps): PopupTypes => { const menuShow = ref(false) const menuOpenClass = ref('') + const disabled = computed(() => props.disabled) // select是否被禁用 + const stopDefault = ref(false) + const updateStopDefaultType = () => { + stopDefault.value = !menuShow.value + } + const openPopup = () => { + if (disabled.value) return menuShow.value = !menuShow.value + updateStopDefaultType() } + watch(menuShow, (status) => { - menuOpenClass.value = status ? 'devui-drop-menu-wrapper' : '' + menuOpenClass.value = dropdownOpenClass(status) }) return { menuShow, + stopDefault, menuOpenClass, + updateStopDefaultType, openPopup, } } diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-single.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-single.ts new file mode 100644 index 0000000000000000000000000000000000000000..52409fa989bbee9bba68d0d32f0fd59905c3738f --- /dev/null +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-single.ts @@ -0,0 +1,48 @@ +/** + * 单选模式 + * */ +import { Ref } from 'vue' +import { CascaderItem, CascaderValueType } from '../src/cascader-types' +/** + * 初始化打开时的视图选中状态 + * 通过value集合获取下标集合 + * @param values 选中的value集合 + * @param curColumn 当前列 + * @param index values数组的起始项,最开始为0 + * @param activeIndexs 当前渲染到视图列的下标集合 + */ +export const initActiveIndexs = (values: CascaderValueType, curColumn: CascaderItem[], index: number, activeIndexs: number[]): void => { + let nextOption = null + for (let i = 0; i < curColumn.length; i++) { + if (curColumn[i]?.value === values[index]) { + nextOption = curColumn[i]?.children + activeIndexs[index] = i + break + } + } + if (index < values.length - 1 && nextOption) { + index += 1 + initActiveIndexs(values, nextOption, index, activeIndexs) + } +} + +/** + * 缓存输入框内容 + * @param inputValueCache 缓存的输入框内容,当最终确定时输出内容 + */ +export const initSingleIptValue = (inputValueCache: Ref): void => { + inputValueCache.value = '' +} + +/** + * 单选选中 + * @param ulIndex 当前操作的列 + * @param valueCache 缓存的当前操作列的value集合 + * @param cascaderItem 当前操作项 + */ +export const singleChoose = (ulIndex: number, valueCache: CascaderValueType, cascaderItem: CascaderItem): void => { + // 删除当前联动级之后的所有级 + valueCache.splice(ulIndex, valueCache.length - ulIndex) + // 更新当前active的value数组 + valueCache[ulIndex] = cascaderItem?.value as number +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/hooks/use-cascader-style.ts b/packages/devui-vue/devui/cascader/hooks/use-cascader-style.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2374e98d5adc39a84eaca63dd5db9c317022f5b --- /dev/null +++ b/packages/devui-vue/devui/cascader/hooks/use-cascader-style.ts @@ -0,0 +1,17 @@ +/** + * 定义组件 style + */ +import { CascaderProps, RootStyleFeedback, CascaderulProps, DropdownStyleFeedback } from '../src/cascader-types' +// 根节点样式 +export const useRootStyle = (props: CascaderProps): RootStyleFeedback => { + return { + inputWidth: `width: ${props.width}px`, + } +} + +// 弹出层样式 +export const useDropdownStyle = (props: CascaderulProps): DropdownStyleFeedback => { + return { + dropdownWidth: `width: ${props?.dropdownWidth}px` + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/index.ts b/packages/devui-vue/devui/cascader/index.ts index 0bb14975fbe22227e75154f17a366370611cd4ec..1e95f9128fa3bdddadc79ad4f2453cc7ae80e626 100644 --- a/packages/devui-vue/devui/cascader/index.ts +++ b/packages/devui-vue/devui/cascader/index.ts @@ -10,7 +10,7 @@ export { Cascader } export default { title: 'Cascader 级联菜单', category: '数据录入', - status: '10%', + status: '30%', install(app: App): void { app.use(Cascader as any) } diff --git a/packages/devui-vue/devui/cascader/src/cascader-types.ts b/packages/devui-vue/devui/cascader/src/cascader-types.ts index 13abc1af5a1e869e2b9ec394dba7cbef6aa2263f..f88ae148ceb189736e5b475751ceeb614fc3e7e0 100644 --- a/packages/devui-vue/devui/cascader/src/cascader-types.ts +++ b/packages/devui-vue/devui/cascader/src/cascader-types.ts @@ -8,12 +8,18 @@ export interface CascaderItem { value: number | string isLeaf?: boolean children?: CascaderItem[] + checked?: boolean + halfChecked?: boolean disabled?: boolean + active?: boolean + _loading?: boolean icon?: string // 用户可以传入自定义属性,并在dropDownItemTemplate中使用 [prop: string]: any } +type CascaderModelValue = number[] +export type CascaderValueType = CascaderModelValue | [CascaderModelValue] export const cascaderProps = { /** * 可选,指定展开次级菜单方式 @@ -25,6 +31,24 @@ export const cascaderProps = { type: String as PropType, default: 'hover' }, + /** + * 可选,单位 px,用于控制组件输入框宽度和下拉的宽度 + * @type { Number | String } + * @default 200 + */ + width: { + type: Number || String, + default: 200 + }, + /** + * 可选,单位 px,控制下拉列表的宽度,默认和组件输入框 width 相等 + * @type { Number | String } + * @default 200 + */ + dropdownWidth: { + type: Number || String, + default: 200 + }, /** * 必选,级联器的菜单信息 * @type {CascaderItem[]} @@ -35,6 +59,31 @@ export const cascaderProps = { default: [], required: true }, + /** + * 可选,级联器是否开启多选模式,开启后为 checkbox 选择 + * @type {Boolean} + * @default false + */ + multiple: { + type: Boolean, + default: false + }, + /** + * 可选,级联器选中项是否显示路径,仅单选模式下生效 + */ + showPath: { + type: Boolean, + default: false + }, + /** + * 可选,需要选中项的value集合 + * @type {CascaderValueType} + * @default [] + */ + value: { + type: Array as PropType, + default: [] + }, /** * 可选,级联器是否禁用 * @type {boolean} @@ -52,8 +101,11 @@ export const cascaderProps = { placeholder: { type: String, default: '' - } - + }, + change: { + type: Function as PropType<(v: CascaderValueType, k: CascaderItem[]) => void>, + default: undefined + }, } as const export type CascaderProps = ExtractPropTypes @@ -61,10 +113,106 @@ export type CascaderProps = ExtractPropTypes export interface PopupTypes { menuShow: Ref menuOpenClass: Ref + stopDefault: Ref openPopup: (e?: MouseEvent) => void + updateStopDefaultType: () => void } +export type CaascaderOptionsType = UnwrapNestedRefs<[CascaderItem[]]> export interface OptionsCallback { - cascaderOptions: never | UnwrapNestedRefs<[CascaderItem[]]> + cascaderOptions: never | CaascaderOptionsType changeCascaderIndexs: (optionItem: CascaderItem, ulIndex: number) => void +} + +// type cascaderItemExtendsProps = 'trigger' +// export type PickCascader = Pick +// export interface CascaderItemNeedType extends PickCascader { + export interface CascaderItemNeedType { + valueCache?: CascaderValueType + trigger?: TriggerTypes + value?: CascaderValueType + inputValueCache?: Ref + confirmInputValueFlg?: Ref + multiple?: boolean + stopDefault?: Ref + activeIndexs?: number[] + tagList?: UnwrapNestedRefs +} +export interface UseCascaderItemCallback { + cascaderItemNeedProps: CascaderItemNeedType + // getInputValue: (a: string, b?: CascaderItem[], c?: Ref) => void +} + +export type CheckedType = 'checked' | 'halfChecked' + +export interface RootStyleFeedback { + inputWidth: string +} + +export const cascaderulProps = { + /** + * 每个ul中的li + * @type {CascaderItem[]} + * @default [] + */ + cascaderItems: { + type: Array as PropType, + default: ():CascaderItem[] => ([{ + label: '', + value: null + }]), + }, + /** + * 可选,单位 px,控制下拉列表的宽度,默认和组件输入框 width 相等 + * @type { Number | String } + * @default 200 + */ + dropdownWidth: { + type: Number || String, + default: 200 + }, + /** + * 当前选中的ul下标 + * @type {Number} + * @default 0 + */ + ulIndex: { + type: Number, + default: 0 + }, + cascaderItemNeedProps: { + type: Object as PropType, + default: ():CascaderItemNeedType => ({}) + }, + stopDefault: { + type: Boolean, + default: false + }, + cascaderOptions: { + type: Array as unknown as PropType<[CascaderItem[]]>, + default: ():[CascaderItem[]] => ([[{ + label: '', + value: null + }]]) + } +} +export type CascaderulProps = ExtractPropTypes + +export interface CascaderItemPropsType extends CascaderulProps { + cascaderItem: CascaderItem + liIndex: number + cascaderItemNeedProps: CascaderItemNeedType +} + +export interface DropdownStyleFeedback { + dropdownWidth: string +} + +export interface MultiplePropsType { + activeOptions: CascaderItem[] + placeholder: string +} + +export interface UpdateStatusCallback { + updateStatus: (node: CascaderItem, options: CaascaderOptionsType, ulIndex: number) => void } \ No newline at end of file diff --git a/packages/devui-vue/devui/cascader/src/cascader.scss b/packages/devui-vue/devui/cascader/src/cascader.scss index cb49189eb72148a52d72dc3eaf1005a43c6a7817..5bed886b42124402706df168daac88843b575fbe 100644 --- a/packages/devui-vue/devui/cascader/src/cascader.scss +++ b/packages/devui-vue/devui/cascader/src/cascader.scss @@ -24,6 +24,12 @@ } } + &__disbaled { + .icon { + color: $devui-disabled-text !important; + } + } + input { width: 100%; padding-right: 16px; diff --git a/packages/devui-vue/devui/cascader/src/cascader.tsx b/packages/devui-vue/devui/cascader/src/cascader.tsx index d0565e61ce12839982a0d1a88ec046e2c3617b28..2cc6e437e5d70ee5bde2504e5d5dfe27a2edf9b8 100644 --- a/packages/devui-vue/devui/cascader/src/cascader.tsx +++ b/packages/devui-vue/devui/cascader/src/cascader.tsx @@ -1,46 +1,168 @@ -import './cascader.scss' +// 公共库 +import { cloneDeep } from 'lodash-es' +import { defineComponent, ref, Ref, reactive, watch, toRef } from 'vue' -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' - +import DMultipleBox from '../components/cascader-multiple/index' +// 事件 +import { cascaderProps, CascaderItem, CascaderProps, CascaderValueType } from './cascader-types' +import { useCascaderItem } from '../hooks/use-cascader-item' +import { useRootClassName } from '../hooks/use-cascader-class' +import { useRootStyle } from '../hooks/use-cascader-style' +import { popupHandles } from '../hooks/use-cascader-popup' +import { initMultipleCascaderItem, initTagList } from '../hooks/use-cascader-multiple' +import { initSingleIptValue, initActiveIndexs } from '../hooks/use-cascader-single' +import './cascader.scss' export default defineComponent({ name: 'DCascader', props: cascaderProps, setup(props: CascaderProps, ctx) { const origin = ref(null) + const cascaderOptions = reactive<[CascaderItem[]]>(cloneDeep([ props?.options ])) + const multiple = toRef(props, 'multiple') + const inputValue = ref('') + const tagList = reactive([]) // 多选模式下选中的值数组,用于生成tag + const rootStyle = useRootStyle(props) + let initIptValue = props.value.length > 0 ? true : false // 有value默认值时,初始化输出内容 + const position = reactive({ - originX: 'left', - originY: 'bottom', - overlayX: 'left', + originX: 'left', + originY: 'bottom', + overlayX: 'left', overlayY: 'top' } as const) // popup弹出层 - const { menuShow, menuOpenClass, openPopup } = popupHandles() + const { menuShow, menuOpenClass, openPopup, stopDefault, updateStopDefaultType } = popupHandles(props) // 配置class - const rootClasses = getRootClass(props, menuShow) - // 级联菜单操作,变换ul、li等 - const { cascaderOptions } = optionsHandles(props.options) + const rootClasses = useRootClassName(props, menuShow) + // 传递给cascaderItem的props + const { cascaderItemNeedProps } = useCascaderItem(props, stopDefault, tagList) + const getInputValue = (label: string, arr: CascaderItem[], inputValueCache: Ref, showPath?: boolean) => { + if (!showPath) { + inputValueCache.value = label + } else { + inputValueCache.value += (label + (arr?.length > 0 ? ' / ' : '')) + } + } + /** + * 控制视图更新 + * 注意视图更新不区分单选或者多选 + * @param activeIndexs 视图展示下标集合 + * @param currentOption 选中的某项 + * @param index value的下标,起始为0 + */ + const updateCascaderView = (value: CascaderValueType, currentOption: CascaderItem[], index: number) => { + if (index === value.length) return + const i = value[index] as number + // 当前的子级 + const current = currentOption[i] + const children = current?.children + if (children?.length > 0) { + // 为下一级增添数据 + cascaderOptions[index + 1] = children + // 递归添加 + updateCascaderView(value, children, index + 1) + } else { + // 当最新的ul(级)没有下一级时删除之前选中ul的数据 + cascaderOptions.splice(index + 1, cascaderOptions.length - 1) + } + } + /** + * 选中项输出 + * 需要区分单选或多选模式 + * @param value 选中值集合 + * @param currentOption 激活的某项 + * @param index value的下标,起始为0 + */ + const updateCascaderValue = (value: CascaderValueType, currentOption: CascaderItem[], index: number) => { + if (!multiple.value) { + // 单选模式 + if (index === value.length) return + const i = value[index] as number + // 当前的子级 + const current = getCurrentOption(currentOption, i) + const children = current?.children + getInputValue(current.label, children, cascaderItemNeedProps.inputValueCache, props.showPath) + if (children?.length > 0) { + updateCascaderValue(value, children, index + 1) + } + } else { + // 多选模式 + const rootColumn = cascaderOptions[0] || [] // 第一列 + value.forEach((targetValue) => { + initMultipleCascaderItem(targetValue, rootColumn, tagList) + }) + } + } + /** + * 根据value筛选每列中选中item + */ + const getCurrentOption = (currentOption: CascaderItem[], i: number) => { + return currentOption.filter(item => item?.value === i)[0] + } + /** + * 监听视图更新 + */ + watch(cascaderItemNeedProps.activeIndexs, val => { + // TODO 多选模式下优化切换选择后的视图切换 + cascaderOptions.splice(val.length, cascaderOptions.length - 1) + updateCascaderView(val, cascaderOptions[0], 0) + }) + /** + * 监听点击最终的节点输出内容 + */ + watch(() => cascaderItemNeedProps.confirmInputValueFlg.value, () => { + // 单选和多选模式初始化 + multiple.value + ? initTagList(tagList) + : initSingleIptValue(cascaderItemNeedProps.inputValueCache) + // 输出确认的选中值 + cascaderItemNeedProps.value = reactive(cloneDeep(cascaderItemNeedProps.valueCache)) + menuShow.value = false + // 点击确定过后禁止再次选中 + updateStopDefaultType() + // 更新值 + updateCascaderValue(cascaderItemNeedProps.value, cascaderOptions[0], 0) + inputValue.value = cascaderItemNeedProps.inputValueCache.value + // 单选模式默认回显视图的选中态 + // 多选模式不默认视图打开状态,因为选中了太多个,无法确定展示哪一种选中态 + if (initIptValue && !multiple.value) { + initActiveIndexs(props.value, cascaderOptions[0], 0, cascaderItemNeedProps.activeIndexs) + initIptValue = false // 只需要初始化一次,之后不再执行 + } + }, { + immediate: true + }) + return () => ( <> -
      - +
      + { multiple.value + ? + : + }
      - +
      {cascaderOptions.map((item, index) => { - return + return })}
      diff --git a/packages/devui-vue/devui/cascader/src/readme.md b/packages/devui-vue/devui/cascader/src/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..70b71288ac00912c79bc1f77230f8444cc047f2d --- /dev/null +++ b/packages/devui-vue/devui/cascader/src/readme.md @@ -0,0 +1,13 @@ +# 组件组成 +cacader组件分为: +- `cascader.tsx`主要文件,渲染主视图 +- `cascader-multiple.tsx`多选模式下的内容展示container,单选模式为input组件 +- `cascader-list.tsx`渲染列 +- `cascader-item.tsx`渲染列中的项 + +# 组件设计 +以下属性在`hooks/use-cascader-item.ts`中 +- `activeIndexs`:每列中的选中下标,负责弹窗交互视图更新 +- `value`:选中的项的value值集合,也可以通过props方式传入 + +**activeIndex** 负责视图更新,也就是hover或者click每项时的交互行为,**value**负责输出选中值。通过`watch`监听这两个值的改变驱动视图和值输出 diff --git a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts index 731d39c95ea013e3b76bcfc6d44114d171185ea4..706048dfb49bf1418fd1b612d9119d0781869674 100644 --- a/packages/devui-vue/devui/dropdown/src/dropdown-types.ts +++ b/packages/devui-vue/devui/dropdown/src/dropdown-types.ts @@ -30,7 +30,7 @@ export const dropdownProps = { closeOnMouseLeaveMenu: { type: Boolean, - default: true + default: false }, showAnimation: { diff --git a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts index 4da866665772dc30d0ab009d275ba650367315d8..ee9cb2fdf1a6e593cfd4e501f46e96b56bb15034 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts +++ b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts @@ -35,6 +35,10 @@ export const editableSelectProps = { loading: { type: Boolean }, + enableLazyLoad: { + type: Boolean, + default: false + }, remoteMethod: { type: Function as PropType<(inputValue: string) => Array> }, diff --git a/packages/devui-vue/devui/editable-select/src/editable-select.tsx b/packages/devui-vue/devui/editable-select/src/editable-select.tsx index 671e6eb9d8f50f288a301937926bc30d9728323b..c6f095bcaffe92d4fda44dca6a7b3cc66393a181 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select.tsx +++ b/packages/devui-vue/devui/editable-select/src/editable-select.tsx @@ -1,58 +1,43 @@ -import { defineComponent, ref, renderSlot, computed, Transition } from "vue" -import { - OptionItem, - editableSelectProps, - EditableSelectProps, -} from "./editable-select-types" -import "./editable-select.scss" -import { Icon } from "../../icon" -import ClickOutside from "../../shared/devui-directive/clickoutside" -import { className } from "./utils" -import { debounce } from "lodash" +import { defineComponent, ref, renderSlot, computed, Transition, watch } from 'vue' +import { OptionItem, editableSelectProps, EditableSelectProps } from './editable-select-types' +import './editable-select.scss' +import { Icon } from '../../icon' +import ClickOutside from '../../shared/devui-directive/clickoutside' +import { className } from './utils' +import { debounce } from 'lodash' export default defineComponent({ - name: "DEditableSelect", + name: 'DEditableSelect', directives: { ClickOutside }, props: editableSelectProps, - emits: ["update:modelValue"], + emits: ['update:modelValue'], setup(props: EditableSelectProps, ctx) { - const inputCls = className( - "devui-form-control devui-dropdown-origin devui-dropdown-origin-open", - { - disabled: props.disabled, - } - ) - - const getLiCls = (item) => { - const { disabledKey } = props - return className("devui-dropdown-item", { - disabled: disabledKey ? !!item[disabledKey] : false, - }) - } - + const dropdownRef = ref(null) const visible = ref(false) - const inputValue = ref("") + const inputValue = ref('') + const activeIndex = ref(0) const query = ref(props.modelValue) const wait = computed(() => (props.remote ? 300 : 0)) const emptyText = computed(() => { const options = filteredOptions.value - if (!props.remote && inputValue.value && options.length === 0) { - return "没有相关记录" + if (!props.remote && options.length === 0) { + return '没有相关记录' } if (options.length === 0) { - return "没有数据" + return '没有数据' } return null }) + const normalizeOptions = computed(() => { let options: OptionItem const { disabledKey } = props - disabledKey ? disabledKey : "disabled" + disabledKey ? disabledKey : 'disabled' return props.options.map((item) => { - if (typeof item !== "object") { + if (typeof item !== 'object') { options = { - name: item, + name: item } return options } @@ -61,9 +46,11 @@ export default defineComponent({ }) const filteredOptions = computed(() => { - const isValidOption = (o: OptionItem): boolean => { + const isValidOption = (o: OptionItem) => { const query = inputValue.value - const containsQueryString = query ? o.name.includes(query) : true + const containsQueryString = query + ? o.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) >= 0 + : true return containsQueryString } return normalizeOptions.value @@ -76,6 +63,12 @@ export default defineComponent({ .filter((item) => item !== null) }) + const findIndex = (o) => { + return normalizeOptions.value.findIndex((item) => { + return item.name === o.name + }) + } + const handleClose = () => { visible.value = false } @@ -84,11 +77,12 @@ export default defineComponent({ visible.value = !visible.value } } - const onInputChange = () => { + + const onInputChange = (val: string) => { if (props.filterMethod) { - props.filterMethod(inputValue.value) + props.filterMethod(val) } else if (props.remote) { - props.remoteMethod(inputValue.value) + props.remoteMethod(val) } } @@ -99,65 +93,83 @@ export default defineComponent({ inputValue.value = value query.value = value if (props.remote) { - debouncedOnInputChange() + debouncedOnInputChange(value) } else { - onInputChange() + onInputChange(value) } } + const selectOptionClick = (e, item) => { const { disabledKey } = props if (disabledKey && item[disabledKey]) { e.stopPropagation() } else { query.value = item.name - ctx.emit("update:modelValue", item.name) + activeIndex.value = findIndex(item) + inputValue.value = '' + ctx.emit('update:modelValue', item.name) } } + + const loadMore = () => { + if (!props.enableLazyLoad) return + const dropdownVal = dropdownRef.value + if (dropdownVal.clientHeight + dropdownVal.scrollTop >= dropdownVal.scrollHeight) { + props.remoteMethod(inputValue.value) + } + } + return () => { + const selectCls = className('devui-form-group devui-has-feedback', { + 'devui-select-open': visible.value + }) + const inputCls = className( + 'devui-form-control devui-dropdown-origin devui-dropdown-origin-open', + { + disabled: props.disabled + } + ) + + const getLiCls = (item, index) => { + const { disabledKey } = props + return className('devui-dropdown-item', { + disabled: disabledKey ? !!item[disabledKey] : false, + selected: activeIndex.value === index + }) + } + return ( -
      - -