diff --git a/devui/transfer/common/use-transfer-base.ts b/devui/transfer/common/use-transfer-base.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff58c15297aea7accbffc91caa9a348455be3817 --- /dev/null +++ b/devui/transfer/common/use-transfer-base.ts @@ -0,0 +1,128 @@ +import { computed, ExtractPropTypes, PropType, ComputedRef } from 'vue' +import { IItem, TState, TResult } from '../types' +import { TransferProps } from './use-transfer' + +export type TransferOperationProps = ExtractPropTypes + +export const transferBaseProps = { + sourceOption: { + type: Array as () => IItem[], + default(): Array { + return [] + } + }, + targetOption: { + type: Array as () => IItem[], + default(): Array { + return [] + } + }, + type: { + type: String, + default: (): string => 'source' + }, + title: { + type: String, + default: (): string => 'Source' + }, + search: { + type: Boolean, + default: (): boolean => false + }, + allChecked: { + type: Boolean, + default: (): boolean => false + }, + query: { + type: String, + default: (): string => '' + }, + alltargetState: { + type: Boolean, + default: (): boolean => false + }, + checkedNum: { + type: Number, + default: (): number => 0 + }, + checkedValues: { + type: Array, + default: (): string[] => [] + }, + allCount: { + type: Number, + default: (): number => 0 + }, + scopedSlots: { + type: Object + }, + onChangeAllSource: { + type: Function as unknown as () => ((val: boolean) => void) + }, + onChangeQuery: { + type: Function as PropType<(val: string) => void> + }, + onUpdateCheckeds: { + type: Function as PropType<(val: string[]) => void> + } +} + +export type TransferBaseProps = ExtractPropTypes + +export const transferOperationProps = { + sourceDisabled: { + type: Boolean, + default: (): boolean => true + }, + targetDisabled: { + type: Boolean, + default: (): boolean => true + }, + onUpdateSourceData: { + type: Function as unknown as () => (() => void) + }, + onUpdateTargetData: { + type: Function as unknown as () => (() => void) + } +} + +const getFilterData = (props, type: string): TResult => { + const newModel: string[] = []; + const data: IItem[] = type === 'source' ? props.sourceOption : props.targetOption + const resultData: IItem[] = data.map((item: IItem) => { + const checked = props.modelValue.some(cur => cur === item.value) + checked && newModel.push(item.value) + return item + }) + return { + model: newModel, + data: resultData + } +} + + + +export const initState = (props: TransferProps, type: string): TState => { + const initModel: TResult = getFilterData(props, type); + const state: TState = { + data: initModel.data, + allChecked: false, + disabled: false, + checkedNum: initModel.model.length, + query: '', + checkedValues: initModel.model, + filterData: initModel.data + } + return state +} + +export const TransferBaseClass = (props: TransferOperationProps): ComputedRef => { + return computed(() => { + return `devui-transfer-panel devui-transfer-${props.type}` + }) +} + +export const Query = ((props: TransferOperationProps): ComputedRef => { + return computed(() => props.query) +}) + diff --git a/devui/transfer/common/use-transfer-operation.ts b/devui/transfer/common/use-transfer-operation.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4eb000f664dd5542a18903ffc0972bc8a2c8d5c --- /dev/null +++ b/devui/transfer/common/use-transfer-operation.ts @@ -0,0 +1,22 @@ + + +export const transferOperationProps = { + sourceDisabled: { + type: Boolean, + default: (): boolean => true + }, + targetDisabled: { + type: Boolean, + default: (): boolean => true + }, + disabled: { + type: Boolean, + default: (): boolean => false + }, + onUpdateSourceData: { + type: Function as unknown as () => (() => void) + }, + onUpdateTargetData: { + type: Function as unknown as () => (() => void) + } +} diff --git a/devui/transfer/common/use-transfer.ts b/devui/transfer/common/use-transfer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eaa39f3f619e8ed63c1dc84d1fcff7ce00a7a71 --- /dev/null +++ b/devui/transfer/common/use-transfer.ts @@ -0,0 +1,58 @@ +import { ExtractPropTypes, PropType } from 'vue' +import { IItem, ITitles, IModel } from '../types' + +export const transferProps = { + sourceOption: { + type: Array as () => IItem[], + require: true, + default(): IItem[] { + return [] + } + }, + targetOption: { + type: Array as () => IItem[], + require: true, + default(): IItem[] { + return [] + } + }, + titles: { + type: Array as PropType, + default: () => (): ITitles[] => ['Source', 'Target'] + }, + modelValue: { + type: Array as PropType, + default: () => (): IModel[] => [], + }, + height: { + type: String, + default: '320px' + }, + isSearch: { + type: Boolean, + default: false + }, + isSourceDroppable: { + type: Boolean, + default: false + }, + isTargetDroppable: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + showOptionTitle: { + type: Boolean, + default: false + }, + slots: { + type: Object + } +} + +export type TransferProps = ExtractPropTypes; + + diff --git a/devui/transfer/index.ts b/devui/transfer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfc390d671d49e450f7e5d9a0cc6bb122b980b1c --- /dev/null +++ b/devui/transfer/index.ts @@ -0,0 +1,16 @@ +import type { App } from 'vue' +import Transfer from './src/transfer' + +Transfer.install = function (app: App) { + app.component(Transfer.name, Transfer) +} + +export { Transfer } + +export default { + title: 'Transfer 穿梭框', + category: '数据录入', + install(app: App): void { + app.use(Transfer as any) + } +} diff --git a/devui/transfer/src/transfer-base.tsx b/devui/transfer/src/transfer-base.tsx new file mode 100644 index 0000000000000000000000000000000000000000..be6c197ac0c44ca783318240aeabb718e39212a5 --- /dev/null +++ b/devui/transfer/src/transfer-base.tsx @@ -0,0 +1,96 @@ +import { defineComponent, computed } from 'vue' +import { transferBaseProps, TransferBaseClass } from '../common/use-transfer-base' +import { 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' +export default defineComponent({ + name: 'DTransferBase', + components: { + DSearch, + DCheckboxGroup, + DCheckbox + }, + props: transferBaseProps, + setup(props: TransferBaseProps, ctx) { + /** data start **/ + const modelValues = computed(() => props.checkedValues) + const searchQuery = computed(() => props.query) + const baseClass = TransferBaseClass(props) + /** data end **/ + + /** watch start **/ + /** watch start **/ + + /** methods start **/ + const updateSearchQuery = (val: string): void => ctx.emit('changeQuery', val) + /** methods start **/ + + return { + baseClass, + searchQuery, + modelValues, + updateSearchQuery + } + }, + render() { + const { + title, + baseClass, + checkedNum, + allChecked, + sourceOption, + allCount, + updateSearchQuery, + search, + searchQuery, + modelValues + } = this + + return ( +
+ { + this.$slots.Header ? this.$slots.Header() : (
+
+ this.$emit('changeAllSource', value)}> + {title} + +
+
{checkedNum}/{allCount}
+
) + } + { + this.$slots.Body ? this.$slots.Body() :
+ {search && } +
+ { + sourceOption.length ? this.$emit('updateCheckeds', values)}> + { + sourceOption.map((item, idx) => { + return + + }) + } + : +
无数据
+ } +
+
+ } +
+ ) + } +}) \ No newline at end of file diff --git a/devui/transfer/src/transfer-operation.tsx b/devui/transfer/src/transfer-operation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9718e3ce08c877200a73d03528636787f83c3d10 --- /dev/null +++ b/devui/transfer/src/transfer-operation.tsx @@ -0,0 +1,25 @@ +import { defineComponent, computed } from 'vue'; +import DButton from '../../button/src/button' +import { transferOperationProps } from '../common/use-transfer-operation' + +export default defineComponent({ + name: 'DTransferOperation', + components: { + DButton + }, + props: transferOperationProps, + setup(props, ctx) { + return () => { + return
+
+ ctx.emit('updateSourceData')}> + ctx.emit('updateTargetData')}> +
+
+ } + }, + data() { + return {} + } +}) \ No newline at end of file diff --git a/devui/transfer/src/transfer.scss b/devui/transfer/src/transfer.scss new file mode 100644 index 0000000000000000000000000000000000000000..c24d4f81c134083e098d55a1071b36bba77be19b --- /dev/null +++ b/devui/transfer/src/transfer.scss @@ -0,0 +1,152 @@ +// @import '../../style/theme/color'; +@import '../../style/theme/color'; + +$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-search-height: 42px; +$devui-transfer-body-list-item-height: 36px; + +.devui-transfer { + display: flex; + + &-panel { + width: 300px; + border: 1px solid $devui-transfer-border-color; + border-radius: $devui-transfer-border-radius; + + &-header { + display: flex; + justify-content: space-between; + height: $devui-transfer-header-height; + line-height: $devui-transfer-header-height; + border-bottom: 1px solid$devui-transfer-header-border-line-color; + + &-allChecked { + display: flex; + margin-left: 20px; + } + + &-num { + margin-right: 12px; + } + } + + &-body { + height: 100%; + + &-search { + height: $devui-transfer-body-search-height; + display: flex; + justify-content: center; + width: calc(100% - 40px); + align-items: center; + margin: 0 auto; + + .devui-search, + .devui-input__wrap { + width: 100%; + } + } + + &-list { + overflow: auto; + height: calc(100% - #{$devui-transfer-header-height} - #{$devui-transfer-body-search-height}); + width: 100%; + + &-item { + margin: 0 20px; + height: $devui-transfer-body-list-item-height; + line-height: $devui-transfer-body-list-item-height; + } + + &-empty { + height: 100%; + // width: 100%; + display: flex; + justify-content: center; + align-items: center; + color: $devui-disabled-text; + } + } + } + + &-operation { + width: 10%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + + &-group { + width: 36px; + + &-left, + &-right { + width: 36px; + height: 36px; + color: $devui-light-text; + background: $devui-primary; + border-radius: 50%; + border: none; + cursor: pointer; + padding: 0; + min-width: 36px !important; + } + + &-right { + margin-top: 12px; + } + + &-left:hover, + &-left span:hover, + &-right:hover, + &-right span:hover { + background: $devui-primary-hover; + } + + &-left:active, + &-left span:active, + &-right:active, + &-right span:active { + background: $devui-primary-active; + } + + &-left:disabled, + &-left span:disabled, + &-right:disabled, + &-right span:disabled { + color: $devui-disabled-text; + background: $devui-disabled-bg; + cursor: not-allowed; + } + } + } + } + + .custom { + &-body { + &-header, + &-list { + display: flex; + justify-content: space-around; + line-height: $devui-transfer-body-list-item-height; + flex-wrap: wrap; + margin: 0; + } + + &-header { + border-bottom: 1px solid $devui-transfer-border-color; + width: 100%; + } + + &-header > div, + &-list > div { + width: 25%; + display: flex; + justify-content: center; + } + } + } +} diff --git a/devui/transfer/src/transfer.tsx b/devui/transfer/src/transfer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dac3cc92473f0734f4a03d377052833dd4a62808 --- /dev/null +++ b/devui/transfer/src/transfer.tsx @@ -0,0 +1,160 @@ +import { defineComponent, reactive, watch, ref } from 'vue' +import { TState } from '../types' +import DTransferBase from './transfer-base' +import DTransferOperation from './transfer-operation' +import { initState } from '../common/use-transfer-base' +import { transferProps, TransferProps } from '../common/use-transfer' +import DCheckbox from '../..//checkbox/src/checkbox' +import './transfer.scss' + +export default defineComponent({ + name: 'DTransfer', + components: { + DTransferBase, + DTransferOperation, + DCheckbox + }, + props: transferProps, + setup(props: TransferProps) { + /** data start **/ + const leftOptions = reactive(initState(props, 'source')) + const rightOptions = reactive(initState(props, 'target')) + const origin = ref(null); + /** data end **/ + + /** watch start **/ + watch( + () => leftOptions.query, + (nVal: string): void => { + searchFilterData(leftOptions) + } + ) + + watch( + () => leftOptions.checkedValues, + (values: string[]): void => { + leftOptions.checkedNum = values.length + setAllCheckedState(leftOptions, values) + }, + { + deep: true + } + ) + + watch( + () => rightOptions.query, + (nVal: string): void => { + searchFilterData(rightOptions) + }, + ) + + watch( + () => rightOptions.checkedValues, + (values: string[]): void => { + rightOptions.checkedNum = values.length; + setAllCheckedState(rightOptions, values) + }, + { + deep: true + } + ) + + /** watch end **/ + + /** methods start **/ + const setAllCheckedState = (source: TState, value: string[]): void => { + if (origin.value === 'click') { + source.allChecked = false + } else { + source.allChecked = value.length === source.data.filter(item => !item.disabled).length ? true : false + } + } + + const updateFilterData = (source: TState, target: TState): void => { + const newData = [] + source.data = source.data.filter(item => { + const hasInclues = source.checkedValues.includes(item.value) + hasInclues && newData.push(item) + return !hasInclues + }) + target.data = target.data.concat(newData) + source.checkedValues = [] + target.disabled = !target.disabled + searchFilterData(source) + searchFilterData(target) + setOrigin('click') + } + const changeAllSource = (source: TState, value: boolean): void => { + if (source.filterData.every(item => item.disabled)) return + source.allChecked = value + if (value) { + source.checkedValues = source.filterData.filter(item => !item.disabled) + .map(item => item.value) + } else { + source.checkedValues = [] + } + setOrigin('change') + } + const updateLeftCheckeds = (values: string[]): void => { + leftOptions.checkedValues = values + setOrigin('change') + } + const updateRightCheckeds = (values: string[]): void => { + rightOptions.checkedValues = values + setOrigin('change') + } + const searchFilterData = (source: TState): void => { + source.filterData = source.data.filter(item => item.key.indexOf(source.query) !== -1) + } + const setOrigin = (value: string): void => { + origin.value = value + } + /** methods end **/ + + return () => { + return
+ changeAllSource(leftOptions, value)} + onUpdateCheckeds={updateLeftCheckeds} + onChangeQuery={(value) => leftOptions.query = value} + /> + 0 ? false : true} + targetDisabled={leftOptions.checkedNum > 0 ? false : true} + onUpdateSourceData={() => { updateFilterData(rightOptions, leftOptions) }} + onUpdateTargetData={() => { updateFilterData(leftOptions, rightOptions) }} + /> + changeAllSource(rightOptions, value)} + onUpdateCheckeds={updateRightCheckeds} + onChangeQuery={(value) => rightOptions.query = value} + /> +
+ } + } +}) \ No newline at end of file diff --git a/devui/transfer/types.ts b/devui/transfer/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c527d74e6661facaf96f8b1c9ec507305194df46 --- /dev/null +++ b/devui/transfer/types.ts @@ -0,0 +1,54 @@ + +export interface IItem { + key: string + value: string + disabled: boolean +} + +export interface ITitles { + [index: number]: string +} + +export interface IModel { + [index: number]: string | number +} + +export interface TState { + data: IItem[] + allChecked: boolean + checkedNum: number + query: string + checkedValues: string[] + filterData: IItem[] + disabled: boolean +} + +export interface TResult { + model: string[] + data: IItem[] +} + + + +// import { ComputedRef } from 'vue' +// export type TItem = { +// key: string +// value: string +// disabled: boolean +// checked?: boolean +// } + +// export type TState = { +// data: TItem[] +// allChecked: boolean +// // disabled: boolean // ComputedRef// +// checkedNum: number +// query: string +// checkedValues: string[] +// filterData: TItem[] +// } + +// export type TResult = { +// model: string[] +// data: TItem[] +// } diff --git a/sites/components/transfer/index.md b/sites/components/transfer/index.md new file mode 100644 index 0000000000000000000000000000000000000000..988204fe24b2687ba999593adb86bae70ce5053b --- /dev/null +++ b/sites/components/transfer/index.md @@ -0,0 +1,224 @@ +# Transfer 穿梭框 + +双栏穿梭选择框。 + +### 何时使用 + +需要在多个可选项中进行多选时。穿梭选择框可用只管的方式在两栏中移动数据,完成选择行为。其中左边一栏为source,右边一栏为target。最终返回两栏的数据,提供给开发者使用。 + +### 基本用法 + +穿梭框基本用法。 + +
+ + +
+ + + +```html + + +``` + +```ts +import {defineComponent, reactive} from 'vue' + type TData = { + id: number + age: number + value: string + disabled?: boolean + } + export default defineComponent({ + setup() { + const options = reactive({ + titles: ['sourceHeader', 'targetHeader'], + source: [ + { + key: '北京', + value: '北京', + disabled: false + }, + { + key: '上海', + value: '上海', + disabled: true + }, + { + key: '广州', + value: '广州', + disabled: false + }, + { + key: '深圳', + value: '深圳', + disabled: false + }, + { + key: '成都', + value: '成都', + disabled: false + }, + { + key: '武汉', + value: '武汉', + disabled: false + }, + { + key: '西安', + value: '西安', + disabled: false + }, + { + key: '福建', + value: '福建', + disabled: false + }, + { + key: '大连', + value: '大连', + disabled: false + }, + { + key: '重庆', + value: '重庆', + disabled: false + } + ], + target: [ + { + key: '南充', + value: '南充', + disabled: false + }, + { + key: '广元', + value: '广元', + disabled: true + }, + { + key: '绵阳', + value: '绵阳', + disabled: false + } + ], + isSearch: true, + modelValues: ['深圳', '成都', '绵阳'] + }) + return { + options + } + } + }) +``` + +### 参数 + +| **参数** | **类型** | **默认** | **说明** | **跳转 Demo** | +| ------------------ | ------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------ | ---------------------------- | +| sourceOption | Array | [] | 可选参数,穿梭框源数据 | [基本用法](#基本用法) | +| targetOption | Array | [] | 可选参数,穿梭框目标数据 | [基本用法](#基本用法) | +| titles | Array | [] | 可选参数,穿梭框标题 | [基本用法](#基本用法) | +| height | string | 320px | 可选参数,穿梭框高度 | [基本用法](#基本用法) | +| isSearch | boolean | true | 可选参数,是否可以搜索 | [基本用法](#基本用法) | +| disabled | boolean | false | 可选参数 穿梭框禁止使用 | [基本用法](#基本用法) |