diff --git a/packages/devui-vue/devui/transfer/common/use-transfer-base.ts b/packages/devui-vue/devui/transfer/common/use-transfer-base.ts index 4c3b613112..d79f3b5011 100644 --- a/packages/devui-vue/devui/transfer/common/use-transfer-base.ts +++ b/packages/devui-vue/devui/transfer/common/use-transfer-base.ts @@ -1,10 +1,13 @@ import { computed, ExtractPropTypes, PropType, ComputedRef } from 'vue' import { IItem, TState, TResult } from '../types' import { TransferProps } from './use-transfer' +import { transferCommon, transferDragFunctions } from './use-transfer-common' export type TransferOperationProps = ExtractPropTypes export const transferBaseProps = { + ...transferCommon, + ...transferDragFunctions, sourceOption: { type: Array as () => IItem[], default(): Array { @@ -57,13 +60,13 @@ export const transferBaseProps = { type: Number, default: (): number => 0 }, - showTooltip: { + isSourceDroppable: { type: Boolean, default: (): boolean => false }, - tooltipPosition: { - type: String as PropType<'top' | 'right' | 'bottom' | 'left'>, - default: (): string => 'top' + isTargetDroppable: { + type: Boolean, + default: (): boolean => false }, scopedSlots: { type: Object @@ -76,6 +79,9 @@ export const transferBaseProps = { }, onUpdateCheckeds: { type: Function as PropType<(val: string[]) => void> + }, + onDragend: { + type: Function as unknown as () => ((dragItem: IItem, dropItem: IItem) => void) } } diff --git a/packages/devui-vue/devui/transfer/common/use-transfer-checkbox.ts b/packages/devui-vue/devui/transfer/common/use-transfer-checkbox.ts new file mode 100644 index 0000000000..1507a161ad --- /dev/null +++ b/packages/devui-vue/devui/transfer/common/use-transfer-checkbox.ts @@ -0,0 +1,17 @@ +import { ExtractPropTypes } from 'vue' +import { IItem } from '../types' +import { transferCommon } from './use-transfer-common' + +const transferCheckboxProps = { + ...transferCommon, + data: { + type: Object as () => IItem, + }, + id: { + type: Number + } +} + +export type TransferCheckboxProps = ExtractPropTypes + +export default transferCheckboxProps \ No newline at end of file diff --git a/packages/devui-vue/devui/transfer/common/use-transfer-common.ts b/packages/devui-vue/devui/transfer/common/use-transfer-common.ts new file mode 100644 index 0000000000..0ad911eb31 --- /dev/null +++ b/packages/devui-vue/devui/transfer/common/use-transfer-common.ts @@ -0,0 +1,34 @@ +import { PropType } from 'vue' +import { IItem } from '../types' + +export const transferCommon = { + showTooltip: { + type: Boolean, + default: (): boolean => false + }, + tooltipPosition: { + type: String as PropType<'top' | 'right' | 'bottom' | 'left'>, + default: (): string => 'top' + } +} + +export const transferDragFunctions = { + onDragstart: { + type: Function as unknown as () => ((event: Event, dragItem: IItem) => void) + }, + onDrop: { + type: Function as unknown as () => ((event: Event, dropItem: IItem) => void) + }, + onDragleave: { + type: Function as unknown as () => ((event: Event, dragItem: IItem) => void) + }, + onDragover: { + type: Function as unknown as () => ((event: Event, dragItem: IItem) => void) + }, + onDragenter: { + type: Function as unknown as () => ((event: Event, dragItem: IItem) => void) + }, + onDragend: { + type: Function as unknown as () => ((event: Event, dropItem: IItem) => void) + } +} diff --git a/packages/devui-vue/devui/transfer/common/use-transfer-drag.ts b/packages/devui-vue/devui/transfer/common/use-transfer-drag.ts new file mode 100644 index 0000000000..be1f9f88f6 --- /dev/null +++ b/packages/devui-vue/devui/transfer/common/use-transfer-drag.ts @@ -0,0 +1,20 @@ +import { PropType, ExtractPropTypes } from '@vue/runtime-core' +import { IItem } from '../types' +import { transferCommon, transferDragFunctions } from './use-transfer-common' + +const transferDragProps = { + ...transferCommon, + ...transferDragFunctions, + itemData: { + type: Object as PropType + }, + id: { + type: Number, + default: (): number | null => null + }, + +} + +export type TransferDragProps = ExtractPropTypes + +export default transferDragProps \ No newline at end of file diff --git a/packages/devui-vue/devui/transfer/common/use-transfer.ts b/packages/devui-vue/devui/transfer/common/use-transfer.ts index 414d2dfc5b..566d8546bb 100644 --- a/packages/devui-vue/devui/transfer/common/use-transfer.ts +++ b/packages/devui-vue/devui/transfer/common/use-transfer.ts @@ -1,7 +1,9 @@ import { ExtractPropTypes, PropType, SetupContext } from 'vue' import { IItem, ITitles, IModel, TState } from '../types' +import { transferCommon } from './use-transfer-common' export const transferProps = { + ...transferCommon, sourceOption: { type: Array as () => IItem[], require: true, @@ -44,14 +46,6 @@ export const transferProps = { type: Boolean, default: (): boolean => false }, - showTooltip: { - type: Boolean, - default: (): boolean => false - }, - tooltipPosition: { - type: String as PropType<'top' | 'right' | 'bottom' | 'left'>, - default: (): string => 'top' - }, beforeTransfer: { type: Function as unknown as () => ((sourceOption: TState, targetOption: TState) => boolean | Promise) }, @@ -72,6 +66,9 @@ export const transferProps = { }, afterTransfer: { type: Function as unknown as () => ((targetOption: TState) => void) + }, + onDragend: { + type: Function as unknown as () => ((direction: string, dragItem: IItem, dropItem: IItem) => void) } } diff --git a/packages/devui-vue/devui/transfer/index.ts b/packages/devui-vue/devui/transfer/index.ts index 2776601564..fb99c5b75d 100644 --- a/packages/devui-vue/devui/transfer/index.ts +++ b/packages/devui-vue/devui/transfer/index.ts @@ -2,7 +2,7 @@ import type { App } from 'vue' import Transfer from './src/transfer' Transfer.install = function (app: App) { - app.component(Transfer.name, Transfer) + app.component(Transfer.name, Transfer) } export { Transfer } @@ -10,7 +10,7 @@ export { Transfer } export default { title: 'Transfer 穿梭框', category: '数据录入', - status: '10%', + status: '40%', install(app: App): void { app.use(Transfer as any) } diff --git a/packages/devui-vue/devui/transfer/src/transfer-base.tsx b/packages/devui-vue/devui/transfer/src/transfer-base.tsx index 10ec9f607d..29411f83f5 100644 --- a/packages/devui-vue/devui/transfer/src/transfer-base.tsx +++ b/packages/devui-vue/devui/transfer/src/transfer-base.tsx @@ -1,51 +1,108 @@ -import { defineComponent, computed } from 'vue' +import { defineComponent, computed, ref, watch, SetupContext } from 'vue' import { transferBaseProps, TransferBaseClass, TransferBaseProps } from '../common/use-transfer-base' import DCheckbox from '../../checkbox/src/checkbox' import DCheckboxGroup from '../../checkbox/src/checkbox-group' import DSearch from '../../search/src/search' -import DTooltip from '../../tooltip/src/tooltip' +import DTransferDrag from './transfer-drag-item' +import DTransfeCheckbox from './transfer-checkbox' export default defineComponent({ name: 'DTransferBase', components: { DSearch, DCheckboxGroup, DCheckbox, - DTooltip + DTransferDrag, + DTransfeCheckbox }, props: transferBaseProps, - setup(props: TransferBaseProps, ctx) { + setup(props: TransferBaseProps, ctx: SetupContext) { /** data start **/ + const allHalfchecked = ref(false)//ref(props.allChecked) const modelValues = computed(() => props.checkedValues as Array) + const dragWrapClass = computed(() => { + const isDrag = props.isSourceDroppable || props.isTargetDroppable + return `devui-transfer-panel-body-list devui-transfer-panel-body-${isDrag ? '' : 'no'}drag` + }) const searchQuery = computed(() => props.filter) const baseClass = TransferBaseClass(props) + const dropItem = ref(null) + /** data end **/ /** watch start **/ + watch( + () => props.checkedNum, + (nVal) => { + if (props.allChecked) { + allHalfchecked.value = !props.allChecked + } else { + allHalfchecked.value = nVal !== 0 + } + }, + { + immediate: true + } + ) /** watch end **/ /** methods start **/ const updateSearchQuery = (val: string): void => ctx.emit('changeQuery', val) - const renderCheckbox = (props, key, showTooltip = false, tooltipPosition = 'top') => { - const checkbox = - - return !showTooltip ? checkbox : {checkbox} + const renderCheckboxGroup = () => { + return ctx.emit('updateCheckeds', values) + }> + { + props.sourceOption.map((item, idx) => { + return + + }) + } + } + + const renderDragCheckboxGroup = () => { + return ctx.emit('updateCheckeds', values) + }> + { + props.sourceOption.map((item, idx) => { + return { + dropItem.value = item + }} + onDragend={(event, dragItem) => { + props.onDragend && props.onDragend(dragItem, dropItem.value) + }} /> + }) + } + + } + /** methods start **/ return { baseClass, searchQuery, + dragWrapClass, modelValues, + dropItem, + allHalfchecked, updateSearchQuery, - renderCheckbox + renderCheckboxGroup, + renderDragCheckboxGroup } }, render() { @@ -54,16 +111,18 @@ export default defineComponent({ baseClass, checkedNum, allChecked, + allHalfchecked, sourceOption, allCount, updateSearchQuery, search, searchQuery, - modelValues, + dragWrapClass, height, - showTooltip, - tooltipPosition, - renderCheckbox, + isSourceDroppable, + isTargetDroppable, + renderCheckboxGroup, + renderDragCheckboxGroup } = this return ( @@ -72,8 +131,11 @@ export default defineComponent({ this.$slots.header ? this.$slots.header() : (
this.$emit('changeAllSource', value)}> + onChange={(value: boolean) => { + this.$emit('changeAllSource', value) + }}> {title}
@@ -86,19 +148,11 @@ export default defineComponent({ {search && } -
+
{ - sourceOption.length ? this.$emit('updateCheckeds', values)}> - { - sourceOption.map((item, idx) => { - return renderCheckbox(item, idx, showTooltip, tooltipPosition) - }) - } - : -
无数据
+ sourceOption.length ? + (isSourceDroppable || isTargetDroppable ? renderDragCheckboxGroup() : renderCheckboxGroup()) + :
无数据
}
diff --git a/packages/devui-vue/devui/transfer/src/transfer-checkbox.tsx b/packages/devui-vue/devui/transfer/src/transfer-checkbox.tsx new file mode 100644 index 0000000000..554277da83 --- /dev/null +++ b/packages/devui-vue/devui/transfer/src/transfer-checkbox.tsx @@ -0,0 +1,42 @@ +import { defineComponent } from 'vue' +import DCheckbox from '../../checkbox/src/checkbox' +import DTooltip from '../../tooltip/src/tooltip' +import transferCheckboxProps, { TransferCheckboxProps } from '../common/use-transfer-checkbox' + +export default defineComponent({ + name: 'DTransferCheckbox', + components: { + DCheckbox, + DTooltip + }, + props: transferCheckboxProps, + setup(props: TransferCheckboxProps) { + /** data start **/ + const renderCheckbox = () => { + return + + } + /** data end **/ + + /** watch start **/ + /** watch end **/ + + /** methods start **/ + /** methods end **/ + + return () => { + return ( + !props.showTooltip ? renderCheckbox() : + {renderCheckbox()} + + ) + } + } +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/transfer/src/transfer-drag-item.tsx b/packages/devui-vue/devui/transfer/src/transfer-drag-item.tsx new file mode 100644 index 0000000000..bb104fd3c4 --- /dev/null +++ b/packages/devui-vue/devui/transfer/src/transfer-drag-item.tsx @@ -0,0 +1,134 @@ +import { defineComponent, ref, onUnmounted } from 'vue' +import transferDragProps, { TransferDragProps } from '../common/use-transfer-drag' +import DTransfeCheckbox from './transfer-checkbox' +import '@devui-design/icons/icomoon/devui-icon.css' + +export default defineComponent({ + name: 'DTransferDrag', + components: { + DTransfeCheckbox + }, + props: transferDragProps, + setup(props: TransferDragProps) { + /** data start **/ + const dragRef = ref(null) + const dragHighlight = ref(false) + const dragOverNodeKey = ref('') + const dropPosition = ref(null) + const dragTimer = ref(null) + /** data end **/ + + /** watch start **/ + /** watch end **/ + + /** methods start **/ + /** + * calcDropPosition: 根据event计算位置 + * event: event对象 + */ + const calcDropPosition = (event): number => { + const { clientY } = event; + const { top, bottom, height } = dragRef.value.getBoundingClientRect(); + const des = Math.max(height * 0.25, 2); + + if (clientY <= top + des) { + return -1; + } + if (clientY >= bottom - des) { + return 1; + } + return 0; + } + /** + * resetState: 重置属性 + */ + const resetState = () => { + dragOverNodeKey.value = '' + dropPosition.value = null + dragHighlight.value = null + } + /** methods end **/ + + /** 生命周期 start **/ + onUnmounted(() => { + clearTimeout(dragTimer.value) + }) + /** 生命周期 end **/ + + return () => { + const state = dragOverNodeKey.value === props.itemData.key + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + clearTimeout(dragTimer.value) + const curDropPosition = calcDropPosition(event) + if (props.itemData.key === dragOverNodeKey.value && curDropPosition === 0) { + resetState() + return + } + dragTimer.value = setTimeout(() => { + dragOverNodeKey.value = props.itemData.key + dropPosition.value = curDropPosition + }, 0); + props.onDragenter && props.onDragenter(event, props.itemData) + }} + onDragover={event => { + event.preventDefault() + event.stopPropagation() + if (props.itemData.key === dragOverNodeKey.value) { + const curDropPosition = calcDropPosition(event) + if (curDropPosition === dropPosition.value) { + return + } + dropPosition.value = curDropPosition; + } + props.onDragover && props.onDragover(event, props.itemData) + }} + onDragleave={event => { + event.stopPropagation() + resetState() + props.onDragleave && props.onDragleave(event, props.itemData) + }} + onDrop={event => { + event.preventDefault() + event.stopPropagation() + resetState() + props.onDrop && props.onDrop(event, props.itemData) + }} + onDragend={event => { + event.stopPropagation() + props.onDragend && props.onDragend(event, props.itemData) + }}> +
{ + event.stopPropagation() + dragHighlight.value = true + props.onDragstart && props.onDragstart(event, props.itemData) + }} + > + + + + + +
+
+ ) + } + } +}) \ No newline at end of file diff --git a/packages/devui-vue/devui/transfer/src/transfer.scss b/packages/devui-vue/devui/transfer/src/transfer.scss index 8a6bb867a2..a1f5234eb9 100644 --- a/packages/devui-vue/devui/transfer/src/transfer.scss +++ b/packages/devui-vue/devui/transfer/src/transfer.scss @@ -4,7 +4,7 @@ $devui-transfer-border-color: #adb0b8; $devui-transfer-border-radius: 2px; $devui-transfer-header-height: 40px; $devui-transfer-header-border-line-color:#dfe1e6; -$devui-transfer-body-list-item-height: 36px; +$devui-transfer-body-list-item-height: 32px; .devui-transfer { display: flex; @@ -52,13 +52,14 @@ $devui-transfer-body-list-item-height: 36px; &-list { overflow: auto; width: 100%; - padding: 0 20px; &-item { height: $devui-transfer-body-list-item-height; line-height: $devui-transfer-body-list-item-height; - width: min-content; - max-width: 100%; + display: flex; + align-items: center; + border-top: 2px solid transparent; + border-bottom: 2px solid transparent; label { text-overflow: ellipsis; @@ -68,14 +69,25 @@ $devui-transfer-body-list-item-height: 36px; } } + &-drag{ + width: 100%; + display: flex; + align-items: center; + &__icon { + padding: 0 4px; + visibility: hidden; + height: 28px; + display: flex; + align-items: center; + } + } + &-drag:hover .devui-transfer-panel-body-list-drag__icon{ + visibility: visible; + } + &-tooltip { - // display: flex; - // max-width: calc(100% - 20px); - // overflow: hidden; - // text-overflow: ellipsis; - // white-space: nowrap; .slotElement { - max-width: 100%; + display: flex; } } @@ -87,6 +99,9 @@ $devui-transfer-body-list-item-height: 36px; color: $devui-disabled-text; } } + &-nodrag{ + padding: 0 20px; + } } &-operation { @@ -98,7 +113,6 @@ $devui-transfer-body-list-item-height: 36px; &-group { width: 36px; - &-left .devui-btn, &-right .devui-btn { width: 36px; @@ -114,4 +128,16 @@ $devui-transfer-body-list-item-height: 36px; } } } + &-drag-dragging{ + background-color: $devui-brand-foil; + } + &-drag-over{ + background-color: $devui-brand-hover; + } +} +.devui-transfer–drag-over-top{ + border-top-color: $devui-brand-active; +} +.devui-transfer–drag-over-bottom { + border-bottom-color: $devui-brand-active; } diff --git a/packages/devui-vue/devui/transfer/src/transfer.tsx b/packages/devui-vue/devui/transfer/src/transfer.tsx index e857d12b82..9fcf636bc0 100644 --- a/packages/devui-vue/devui/transfer/src/transfer.tsx +++ b/packages/devui-vue/devui/transfer/src/transfer.tsx @@ -1,5 +1,5 @@ import { defineComponent, reactive, watch, ref, SetupContext } from 'vue' -import { TState } from '../types' +import { TState, IItem } from '../types' import DTransferBase from './transfer-base' import DTransferOperation from './transfer-operation' import { initState } from '../common/use-transfer-base' @@ -39,7 +39,7 @@ export default defineComponent({ watch( () => leftOptions.keyword, - (nVal: string): void => { + (): void => { searchFilterData(leftOptions) } ) @@ -57,7 +57,7 @@ export default defineComponent({ watch( () => rightOptions.keyword, - (nVal: string): void => { + (): void => { searchFilterData(rightOptions) }, ) @@ -155,6 +155,14 @@ export default defineComponent({ const isFunction = (type: string): boolean => { return props[type] && typeof props[type] === 'function' } + + const dataSort = (target: TState, dragItem: IItem, dropItem: IItem, direction: string) => { + const startIndex = target.filterData.findIndex(item => item.key === dragItem.key) + const endIndex = target.filterData.findIndex(item => item.key === dropItem.key) + target.filterData.splice(endIndex, 1, dragItem) + target.filterData.splice(startIndex, 1, dropItem) + props.onDragend && props.onDragend(direction, dragItem, dropItem) + } /** methods end **/ return () => { @@ -172,6 +180,7 @@ export default defineComponent({ allCount={leftOptions.data.length} showTooltip={props.showTooltip} tooltipPosition={props.tooltipPosition} + isSourceDroppable={props.isSourceDroppable} v-slots={ { header: headerSlot(ctx, 'left'), @@ -181,6 +190,7 @@ export default defineComponent({ onChangeAllSource={(value) => changeAllSource(leftOptions, value)} onUpdateCheckeds={updateLeftCheckeds} onChangeQuery={(value) => changeQueryHandle(leftOptions, 'left', value)} + onDragend={(dragItem, dropItem) => dataSort(leftOptions, dragItem, dropItem, 'left')} /> changeAllSource(rightOptions, value)} onUpdateCheckeds={updateRightCheckeds} onChangeQuery={(value) => changeQueryHandle(rightOptions, 'right', value)} + onDragend={(dragItem, dropItem) => dataSort(rightOptions, dragItem, dropItem, 'right')} />
} diff --git a/packages/devui-vue/docs/components/transfer/index.md b/packages/devui-vue/docs/components/transfer/index.md index 2a1ae1409e..23ddee3462 100644 --- a/packages/devui-vue/docs/components/transfer/index.md +++ b/packages/devui-vue/docs/components/transfer/index.md @@ -7,7 +7,7 @@ 需要在多个可选项中进行多选时。穿梭选择框可用只管的方式在两栏中移动数据,完成选择行为。其中左边一栏为 source,右边一栏为 target。最终返回两栏的数据,提供给开发者使用。 ### 基本用法 - +穿梭框基本用法。 :::demo ```vue @@ -67,6 +67,24 @@ export default defineComponent({ value: '福建', disabled: false, }, + + ] + const originTarget = [ + { + key: '南充', + value: '南充', + disabled: false, + }, + { + key: '广元', + value: '广元', + disabled: false, + }, + { + key: '绵阳', + value: '绵阳', + disabled: false, + }, { key: '大连', value: '大连', @@ -78,6 +96,95 @@ export default defineComponent({ disabled: false, }, ] + + const options = reactive({ + titles: ['sourceHeader', 'targetHeader'], + source: originSource, + target: originTarget, + originSource, + originTarget, + isSearch: true, + modelValues: ['深圳', '成都', '绵阳'] + }) + + return { + options + } + } +}) + +``` + +::: + + +### 拖拽排序 + +可以对穿梭框源和目标框的数据进行排序。 + +:::demo + +```vue + + ``` -::: +::: ### 自定义穿梭框 - +可以对穿梭框内容的显示进行自定义。 :::demo ```vue @@ -350,6 +469,7 @@ export default defineComponent({ height: 36px; line-height: 36px; border-bottom: 1px solid #dfe1e6; + align-items: center; } .custom-transfer__body__list__part { width: 30%; @@ -377,6 +497,8 @@ export default defineComponent({ ::: + + ### API d-transfer 参数 @@ -397,4 +519,13 @@ d-transfer 事件 | transferToTarget | `EventEmitter<{sourceOption, targetOption}>` | 当点击左穿梭时,返回穿梭框源和目标数据; | [基本用法](#基本用法) | | searching | `EventEmitter<{direction, keyword}>` | 当搜索时触发,返回目标穿梭框和搜索文字,不设置此事件则会使用默认方法; | [基本用法](#基本用法) | | transferring | `EventEmitter` | 当穿梭时触发,返回目标穿梭框,不设置此事件则会使用默认方法; | [基本用法](#基本用法) | -| afterTransfer | `EventEmitter` | 当穿梭完成后,返回目标穿梭框,不设置transferEvent才会触发; | [基本用法](#基本用法) | \ No newline at end of file +| afterTransfer | `EventEmitter` | 当穿梭完成后,返回目标穿梭框,不设置transferEvent才会触发; | [基本用法](#基本用法) | +| onDragEnd | `(direction: string, dragItem: TransferItem, dropItem: TransferItem) => void` | 节点结束拖拽的回调; | [基本用法](#基本用法) | + + +d-transfer.Item +| **属性** | **类型** | **默认** | **说明** | **跳转 Demo** | +| ------------------ | ------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------ | ---------------------------- | +| key | `string (required)` | - | 选项的键值(唯一标识符) | [基本用法](#基本用法) | +| value | `string (required)` | - | 选项对应的值 | [基本用法](#基本用法) | +| disabled | `boolean` | - | 是否禁用此选项 | [基本用法](#基本用法) | \ No newline at end of file