feat: 穿梭框选择组件
This commit is contained in:
217
packages/select-transfer-modal/index.vue
Normal file
217
packages/select-transfer-modal/index.vue
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: zhaojinfeng 121016171@qq.com
|
||||||
|
* @Date: 2023-08-14 11:07:15
|
||||||
|
* @LastEditors: zhaojinfeng 121016171@qq.com
|
||||||
|
* @LastEditTime: 2023-08-14 16:27:08
|
||||||
|
* @FilePath: \vue3\packages\select-transfer-modal\index.vue
|
||||||
|
* @Description:
|
||||||
|
*
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-config-provider :locale="locale">
|
||||||
|
<el-dialog v-model="showDialog" class="select-transfer-modal" :title="title" :width="width" append-to-body :z-index="zIndex" @open="getList" @closed="closed">
|
||||||
|
<el-form ref="queryRef" :model="queryParams" inline>
|
||||||
|
<el-form-item v-for="formItem in formItems" :key="formItem.prop" :label="formItem.label" :prop="formItem.prop">
|
||||||
|
<el-select v-if="formItem.selectOptions" :placeholder="formItem.placeholder">
|
||||||
|
<el-option v-for="option in formItem.selectOptions" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<el-input v-else v-model="queryParams[formItem.prop]" :placeholder="formItem.placeholder" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="loading" @click="getList">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button :loading="loading" @click="resetQuery">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-transfer
|
||||||
|
v-model="transferValue"
|
||||||
|
v-loading="loading"
|
||||||
|
inline-block
|
||||||
|
text-left
|
||||||
|
style="--el-transfer-panel-width:25vw"
|
||||||
|
:titles="['待选项', '已选项']"
|
||||||
|
:button-texts="['移除', '添加']"
|
||||||
|
:format="{
|
||||||
|
noChecked: '${total}',
|
||||||
|
hasChecked: '${checked}/${total}',
|
||||||
|
}"
|
||||||
|
:data="data"
|
||||||
|
:filter-method="filterMethod"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<template #default="{ option }">
|
||||||
|
<slot :option="option">
|
||||||
|
<div flex h-100px>
|
||||||
|
<div grow>
|
||||||
|
{{ option.name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="optionRight">
|
||||||
|
{{ option[optionRight] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<template #left-footer>
|
||||||
|
<el-pagination
|
||||||
|
v-if="!hidePagination && total > queryParams.pageSize"
|
||||||
|
v-model:current-page="queryParams.pageNum"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
background
|
||||||
|
small
|
||||||
|
layout="total, sizes, prev, next, jumper"
|
||||||
|
mt-9px
|
||||||
|
:page-sizes="pageSizes"
|
||||||
|
:pager-count="pagerCount"
|
||||||
|
:total="total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="getList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-transfer>
|
||||||
|
<template #footer>
|
||||||
|
<el-space>
|
||||||
|
<el-button type="primary" @click="confirm">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="showDialog = false">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
</el-space>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="ThSelectTransferModal">
|
||||||
|
import type { FormInstance, TransferDataItem, TransferKey, TransferPropsAlias } from 'element-plus'
|
||||||
|
import locale from 'element-plus/lib/locale/lang/zh-cn'
|
||||||
|
|
||||||
|
const properties = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
value?: TransferKey[]
|
||||||
|
show: boolean
|
||||||
|
defaultData?: Record<string, any>[]
|
||||||
|
defaultParams?: Record<string, any>
|
||||||
|
formItems?: FormItem[]
|
||||||
|
request: (args: any) => Promise<Page>
|
||||||
|
layout?: string
|
||||||
|
optionRight?: string
|
||||||
|
pageSizes?: number[]
|
||||||
|
pagerCount?: number
|
||||||
|
hideButton?: boolean
|
||||||
|
hidePagination?: boolean
|
||||||
|
title?: string
|
||||||
|
width?: string | number
|
||||||
|
zIndex?: number
|
||||||
|
props?: TransferPropsAlias
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
props: () => ({ key: 'id', label: 'name', disabled: 'disabled' }),
|
||||||
|
show: false,
|
||||||
|
defaultParams: () => ({}),
|
||||||
|
pageSizes: () => [10, 20, 30, 50],
|
||||||
|
pagerCount: document.body.clientWidth < 992 ? 5 : 7,
|
||||||
|
hidePagination: false,
|
||||||
|
title: '选择',
|
||||||
|
width: '70%',
|
||||||
|
zIndex: 2023,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const value = useVModel(properties, 'value')
|
||||||
|
const showDialog = useVModel(properties, 'show')
|
||||||
|
|
||||||
|
const transferValue = $ref<TransferKey[]>([])
|
||||||
|
|
||||||
|
const temp = reactive({})
|
||||||
|
|
||||||
|
let currentIdList = $ref<any[]>([])
|
||||||
|
|
||||||
|
function filterMethod(_query: string, item: TransferDataItem) {
|
||||||
|
return currentIdList.includes(item[properties.props.key]) || transferValue.includes(item[properties.props.key])
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = computed(() => {
|
||||||
|
const all: any[] = []
|
||||||
|
for (const key in temp)
|
||||||
|
all.push(temp[key])
|
||||||
|
|
||||||
|
return all
|
||||||
|
})
|
||||||
|
|
||||||
|
let total = $ref(0)
|
||||||
|
let loading = $ref(false)
|
||||||
|
|
||||||
|
/* 表单绑定的ref */
|
||||||
|
const queryRef = $ref<FormInstance>(null)
|
||||||
|
|
||||||
|
/** 查询表格数据 */
|
||||||
|
async function getList() {
|
||||||
|
loading = true
|
||||||
|
try {
|
||||||
|
const response = await properties.request({
|
||||||
|
...properties.defaultParams,
|
||||||
|
...queryParams,
|
||||||
|
})
|
||||||
|
currentIdList = response.rows.map((e) => {
|
||||||
|
temp[e[properties.props.key]] = e
|
||||||
|
return e[properties.props.key]
|
||||||
|
})
|
||||||
|
total = response.total
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
value.value = [...transferValue]
|
||||||
|
showDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function closed() {
|
||||||
|
queryRef?.resetFields()
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
currentIdList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
function resetQuery() {
|
||||||
|
queryRef?.resetFields()
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
if (queryParams.pageNum * val > total)
|
||||||
|
queryParams.pageNum = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.defaultData?.forEach((e) => {
|
||||||
|
temp[e[properties.props.key]] = e
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => properties.defaultData, (data) => {
|
||||||
|
data?.forEach((e) => {
|
||||||
|
temp[e[properties.props.key]] = e
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.select-transfer-modal{
|
||||||
|
.el-checkbox:last-of-type{
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
84
stories/SelectTransferModal.stories.ts
Normal file
84
stories/SelectTransferModal.stories.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* @Author: zhaojinfeng 121016171@qq.com
|
||||||
|
* @Date: 2023-08-14 11:07:15
|
||||||
|
* @LastEditors: zhaojinfeng 121016171@qq.com
|
||||||
|
* @LastEditTime: 2023-08-14 16:45:09
|
||||||
|
* @FilePath: \vue3\stories\SelectTransferModal.stories.ts
|
||||||
|
* @Description:
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||||
|
import ThSelectTransferModal from '../packages/select-transfer-modal/index.vue'
|
||||||
|
|
||||||
|
// 模拟800毫秒钟的接口请求
|
||||||
|
function request(e: Record<'pageNum' | 'pageSize', number>) {
|
||||||
|
return new Promise<Page>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const rows: any[] = []
|
||||||
|
for (let index = 0; index < e.pageSize; index++) {
|
||||||
|
const id = (e.pageNum - 1) * e.pageSize + index
|
||||||
|
rows.push({
|
||||||
|
id,
|
||||||
|
name: `name${id}`,
|
||||||
|
value: `value${id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
rows,
|
||||||
|
total: 9999,
|
||||||
|
message: 'ok',
|
||||||
|
})
|
||||||
|
}, 800)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: '表单组件/SelectTransferModal',
|
||||||
|
tags: ['表单组件', 'autodocs'],
|
||||||
|
args: {
|
||||||
|
value: [2],
|
||||||
|
show: false,
|
||||||
|
defaultParams: {},
|
||||||
|
formItems: [{ label: 'label1', prop: 'prop1' }, { label: 'label2', prop: 'prop2', selectOptions: [{ label: '1', value: '1' }, { label: '2', value: '2' }] }],
|
||||||
|
request,
|
||||||
|
optionRight: 'value',
|
||||||
|
layout: 'total, sizes, prev, pager, next, jumper',
|
||||||
|
pageSizes: [10, 20, 30, 50],
|
||||||
|
pagerCount: document.body.clientWidth < 992 ? 5 : 7,
|
||||||
|
props: { key: 'id', label: 'name', disabled: 'disabled' },
|
||||||
|
hidePagination: false,
|
||||||
|
title: '选择',
|
||||||
|
width: '70%',
|
||||||
|
zIndex: 2023,
|
||||||
|
},
|
||||||
|
render: args => ({
|
||||||
|
components: { ThSelectTransferModal },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: `<th-select-transfer-modal
|
||||||
|
v-model:value="args.value"
|
||||||
|
v-model:show="args.show"
|
||||||
|
:form-items="args.formItems"
|
||||||
|
:default-params="args.defaultParams"
|
||||||
|
:hide-pagination="args.hidePagination"
|
||||||
|
:request="args.request"
|
||||||
|
:layout="args.layout"
|
||||||
|
:optionRight="args.optionRight"
|
||||||
|
:page-sizes="args.pageSizes"
|
||||||
|
:pager-count="args.pagerCount"
|
||||||
|
:props="args.props"
|
||||||
|
:title="args.title"
|
||||||
|
:width="args.width"
|
||||||
|
:z-index="args.zIndex"
|
||||||
|
/>`,
|
||||||
|
}),
|
||||||
|
} satisfies Meta<typeof ThSelectTransferModal>
|
||||||
|
export default meta
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Base: Story = {
|
||||||
|
name: '基本使用',
|
||||||
|
}
|
4
types/auto-imports.d.ts
vendored
4
types/auto-imports.d.ts
vendored
@ -4,8 +4,8 @@
|
|||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const $$: typeof import('vue/macros')['$$']
|
|
||||||
const $: typeof import('vue/macros')['$']
|
const $: typeof import('vue/macros')['$']
|
||||||
|
const $$: typeof import('vue/macros')['$$']
|
||||||
const $computed: typeof import('vue/macros')['$computed']
|
const $computed: typeof import('vue/macros')['$computed']
|
||||||
const $customRef: typeof import('vue/macros')['$customRef']
|
const $customRef: typeof import('vue/macros')['$customRef']
|
||||||
const $ref: typeof import('vue/macros')['$ref']
|
const $ref: typeof import('vue/macros')['$ref']
|
||||||
@ -307,7 +307,6 @@ declare module 'vue' {
|
|||||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||||
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
||||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
|
|
||||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
@ -596,7 +595,6 @@ declare module '@vue/runtime-core' {
|
|||||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||||
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
|
||||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
|
|
||||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
|
2
types/components.d.ts
vendored
2
types/components.d.ts
vendored
@ -27,6 +27,7 @@ declare module 'vue' {
|
|||||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTransfer: typeof import('element-plus/es')['ElTransfer']
|
||||||
Header: typeof import('./../packages/header/index.vue')['default']
|
Header: typeof import('./../packages/header/index.vue')['default']
|
||||||
MaskText: typeof import('./../packages/mask-text/index.vue')['default']
|
MaskText: typeof import('./../packages/mask-text/index.vue')['default']
|
||||||
PreviewOffice: typeof import('./../packages/preview-office/index.vue')['default']
|
PreviewOffice: typeof import('./../packages/preview-office/index.vue')['default']
|
||||||
@ -34,6 +35,7 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SelectTableModal: typeof import('./../packages/select-table-modal/index.vue')['default']
|
SelectTableModal: typeof import('./../packages/select-table-modal/index.vue')['default']
|
||||||
|
SelectTransferModal: typeof import('./../packages/select-transfer-modal/index.vue')['default']
|
||||||
UploadAvatar: typeof import('./../packages/upload-avatar/index.vue')['default']
|
UploadAvatar: typeof import('./../packages/upload-avatar/index.vue')['default']
|
||||||
UploadSingleFile: typeof import('./../packages/upload-single-file/index.vue')['default']
|
UploadSingleFile: typeof import('./../packages/upload-single-file/index.vue')['default']
|
||||||
UploadTable: typeof import('./../packages/upload-table/index.vue')['default']
|
UploadTable: typeof import('./../packages/upload-table/index.vue')['default']
|
||||||
|
Reference in New Issue
Block a user