特性:
- 表格宽度可以自定义
- 翻页器显示控件可以自定义
- 列配置项可以设置显示字段列名称、宽度、字段名
- 可以配置搜索框提示文本,支持搜索过滤
- 穿梭框顶部标题可以自定义
- 左右箭头按钮文本可以设置
sgTransfer源码
<template> <div :class="$options.name"> <div class="sg-start" :style="{ width: width, height: height, ...style, ...style_start }" > <div class="sg-title" v-if="titles"> {{ titles[0] }} </div> <div class="sg-search"> <el-input style="width: 100%" v-model.trim="inputSearchValue_start" maxlength="20" :show-word-limit="false" :placeholder="filterPlaceholder || `请输入搜索内容...`" clearable @keyup.native.enter="initListStart" @clear="initListStart" > <el-button slot="append" icon="el-icon-search" @click="initListStart" /> </el-input> </div> <div class="sg-table"> <el-table ref="table_start" :data="tableData_start" :header-cell-style="{ background: '#f5f7fa' }" :height="height ? `calc(${height} - 150px)` : '300px'" style="width: 100%" stripe @selection-change="selection_start_change" :row-class-name="row_class_name" @row-click="row_click_start" @row-dblclick="row_dblclick_start" > <el-table-column type="selection" minWidth="50" :selectable="selectable" /> <el-table-column v-for="(a, i) in tableItems_start" :key="i" :prop="a.prop" :label="a.label" :width="a.width || false" :minWidth="a.minWidth || false" show-overflow-tooltip /> </el-table> </div> <div class="sg-pagination"> <el-pagination background :hidden="startPage.total <= 10" :layout="layout" :page-sizes="[10, 20, 50]" :pager-count="5" :current-page.sync="startPage.currentPage" :page-size.sync="startPage.pageSize" :total="startPage.total" @size-change="pageChange" @current-change="pageChange" /> </div> </div> <div class="sg-center"> <el-button :disabled="disabledLeftButton" @click="remove" type="primary" icon="el-icon-arrow-left" >{{ buttonTexts ? buttonTexts[0] : "" }}</el-button > <el-button :disabled="disabledRightButton" @click="add" type="primary" >{{ buttonTexts ? buttonTexts[1] : "" }}<i class="el-icon-arrow-right" style="margin-left: 5px"></i ></el-button> </div> <div class="sg-end" :style="{ width: width, height: height, ...style, ...style_end }"> <div class="sg-title" v-if="titles"> {{ titles[1] }} </div> <div class="sg-search"> <el-input style="width: 100%" v-model.trim="inputSearchValue_end" maxlength="20" :show-word-limit="false" :placeholder="filterPlaceholder || `请输入搜索内容...`" clearable @keyup.native.enter="initListEnd({ currentPage: 1 })" @clear="initListEnd" > <el-button slot="append" icon="el-icon-search" @click="initListEnd({ currentPage: 1 })" /> </el-input> </div> <div class="sg-table"> <el-table ref="table_end" :data="tableData_end" :header-cell-style="{ background: '#f5f7fa' }" :height="height ? `calc(${height} - 150px)` : '300px'" style="width: 100%" stripe @selection-change="selection_end_change" @row-click="row_click_end" > <el-table-column type="selection" minWidth="50" /> <el-table-column v-for="(a, i) in tableItems_end" :key="i" :prop="a.prop" :label="a.label" :width="a.width || false" :minWidth="a.minWidth || false" show-overflow-tooltip /> </el-table> </div> <div class="sg-pagination"> <el-pagination background :hidden="endPage.total <= 10" :layout="layout" :page-sizes="[10, 20, 50]" :pager-count="5" :current-page.sync="endPage.currentPage" :page-size.sync="endPage.pageSize" :total="endPage.total" @size-change="initListEnd" @current-change="initListEnd" /> </div> </div> </div> </template> <script> export default { name: "sgTransfer", data() { return { width: "200px", //穿梭框宽度 height: null, //穿梭框高度 style: {}, //全局穿梭框样式 style_start: {}, //左侧穿梭框样式 style_end: {}, //右侧穿梭框样式 layout: `total, sizes, prev, pager, next, jumper`, disabledForm: false, inputSearchValue_start: "", inputSearchValue_end: "", tableItems_start: [], //表格列配置项 tableItems_end: [], //表格列配置项 tableData_start: [], //呈现的当前页数据 tableData_end: [], //呈现的当前页数据 tableData_end_bk: [], //最终选择的数据 selection_start: [], selection_end: [], startPage: { currentPage: 1, pageSize: 10, total: 0 }, endPage: { currentPage: 1, pageSize: 10, total: 0 }, mainKey: null, //主键 filterKey_end: null, //右侧穿梭表过滤关键词字段 }; }, props: [ "value", "data", /*格式说明 data: { width: '400px',//表格宽度 layout: `total, sizes, prev, next, jumper`,//翻页器显示控件 // 列配置项 tableItems: [ { prop: 'ID', label: '工号', minWidth: '50' }, { prop: 'XM', label: '姓名', minWidth: '50' }, { prop: 'YHM', label: '用户名', minWidth: '50' }, ], tableData: [],//表格显示内容 startPage: { total: 0, },//实际总数 }, */ "titles", "buttonTexts", "filterPlaceholder", ], computed: { disabledLeftButton(d) { return this.selection_end.length === 0; }, disabledRightButton(d) { // 在左边表格选中项里面,遍历每一项,如果在右侧表格中都能找到匹配项就true return this.selection_start.every((row) => this.tableData_end_bk.some((v) => this.isSameItem(v, row)) ); }, }, watch: { value: { handler(d) { // 避免重复循环执行双向绑定 if ( d && JSON.stringify(JSON.parse(JSON.stringify(this.tableData_end_bk)).sort()) !== JSON.stringify(JSON.parse(JSON.stringify(d)).sort()) ) { this.inputSearchValue_start = ""; this.inputSearchValue_end = ""; this.startPage.currentPage = 1; this.endPage.currentPage = 1; this.tableData_end_bk = d || []; this.initListStart(); } else this.initListStart(); }, deep: true, immediate: true, }, data: { handler(d) { if (d) { d.width && (this.width = d.width); d.height && (this.height = d.height); d.style && (this.style = d.style); d.style_start && (this.style_start = d.style_start); d.style_end && (this.style_end = d.style_end); d.layout && (this.layout = d.layout); this.tableData_start = d.tableData; this.tableItems_start = d.tableItems_start || d.tableItems; this.tableItems_end = d.tableItems_end || d.tableItems; this.mainKey = (this.tableItems_start.find((v) => v.mainKey) || {}).prop; //主键 this.filterKey_end = ( this.tableItems_start.find((v) => v.filterKey_end) || {} ).prop; //右侧穿梭表过滤关键词字段 this.startPage.total = (d.startPage || {}).total || 0; this.$nextTick(() => { this.refreshCheckStatus(); }); // 刷新勾选状态 } }, deep: true, immediate: true, }, tableData_end_bk: { handler(d) { this.$emit(`input`, d); this.initListEnd(); if (this.tableData_end.length === 0) { this.inputSearchValue_end = ""; this.endPage.currentPage = Math.round( this.tableData_end_bk.length / this.endPage.pageSize ); } }, deep: true, immediate: true, }, }, methods: { // 双击选中移动到右侧 row_dblclick_start(row, column, event) { this.$refs.table_start.toggleRowSelection(row, true); this.add(); }, row_click_start(row, column, event) { this.$refs.table_start.toggleRowSelection(row); }, row_click_end(row, column, event) { this.$refs.table_end.toggleRowSelection(row); }, pageChange(d) { this.initListStart(); }, // 刷新勾选状态 refreshCheckStatus() { this.tableData_start.forEach((row) => this.$refs.table_start.toggleRowSelection( row, this.tableData_end_bk.some((v) => this.isSameItem(v, row)) ) ); }, selectable(row) { return !this.tableData_end_bk.some((v) => this.isSameItem(v, row)); }, row_class_name({ row, rowIndex }) { return this.tableData_end_bk.find((v) => this.isSameItem(v, row)) ? "selected" : ""; }, isSameItem(a_obj, b_obj) { let isSame = true; if (this.mainKey) { isSame = a_obj[this.mainKey] == b_obj[this.mainKey]; } else { isSame = Object.keys(a_obj).every((k) => a_obj[k] == b_obj[k]); } return isSame; }, remove(d) { if (this.mainKey) { let selection_end_mainKeys = this.selection_end.map((v) => v[this.mainKey]); this.tableData_end_bk = this.tableData_end_bk.filter( (v) => !selection_end_mainKeys.includes(v[this.mainKey]) ); } else { let selection_end = this.selection_end.map((v) => JSON.stringify(v)); this.tableData_end_bk = this.tableData_end_bk.filter( (v) => !selection_end.includes(JSON.stringify(v)) ); } this.$nextTick(() => { this.refreshCheckStatus(); }); // 刷新勾选状态 }, add(d) { this.selection_start.forEach( (row) => this.tableData_end_bk.some((v) => this.isSameItem(v, row)) || this.tableData_end_bk.push(row) ); this.$nextTick(() => { this.refreshCheckStatus(); }); // 刷新勾选状态 }, selection_start_change(selection) { this.selection_start = selection; }, selection_end_change(selection) { this.selection_end = selection; }, initListStart() { this.$emit("init", { keyword: this.inputSearchValue_start, currentPage: this.startPage.currentPage || 1, pageSize: this.startPage.pageSize, }); }, initListEnd({ keyword = this.inputSearchValue_end, currentPage = this.endPage.currentPage || 1, pageSize = this.endPage.pageSize, } = {}) { this.endPage.currentPage = currentPage; this.endPage.pageSize = pageSize; let results = []; if (this.filterKey_end) { results = this.tableData_end_bk.filter((obj) => keyword ? (obj[this.filterKey_end] || "") .toString() .toLocaleLowerCase() .includes(keyword.toString().toLocaleLowerCase()) : true ); } else { results = this.tableData_end_bk.filter((obj) => keyword ? Object.keys(obj).some((k) => (obj[k] || "") .toString() .toLocaleLowerCase() .includes(keyword.toString().toLocaleLowerCase()) ) : true ); } this.endPage.total = results.length; this.tableData_end = results.slice( (currentPage - 1) * pageSize, currentPage * pageSize ); }, }, }; </script> <style lang="scss" scoped> .sgTransfer { display: flex; align-items: center; flex-wrap: nowrap; white-space: nowrap; & > .sg-start, & > .sg-end { border: 1px solid #ebeef5; border-radius: 4px; overflow: hidden; background: #fff; display: inline-block; vertical-align: middle; max-height: 100%; box-sizing: border-box; position: relative; .sg-title { height: 40px; line-height: 40px; background: #f5f7fa; margin: 0; padding-left: 15px; border-bottom: 1px solid #ebeef5; box-sizing: border-box; color: #000; } .sg-search { box-sizing: border-box; padding: 10px; } .sg-table { } .sg-pagination { height: 50px; display: flex; justify-content: center; width: 100%; box-sizing: border-box; padding: 10px; } } & > .sg-center { margin: 0 10px; } & > .sg-end { } } >>> .el-table { tr.selected { filter: brightness(0.95); pointer-events: none; } .el-table__cell.gutter { border-bottom: 1px solid #ebeef5; background-color: #f5f7fa; } } </style>
用例
<template> <div> <sgTransfer v-model="transferValue" :data="transferData" :titles="['所有用户', '本组成员']" :button-texts="['到左边', '到右边']" :filter-placeholder="`请输入工号、姓名…`" @init="initTransfer" /> <hr /> <div> <h1>勾选的数据transferValue:</h1> <div v-html="JSON.stringify(transferValue).replace(/\,\{/g, ',\n{')" style="word-wrap: break-word; word-break: break-all; white-space: break-spaces" ></div> </div> </div> </template> <script> import sgTransfer from "@/vue/components/admin/sgTransfer"; export default { components: { sgTransfer }, data() { return { // 穿梭框配置项 transferValue: [], transferData: { width: "400px", //表格宽度 height: "calc(100vh - 200px)", //表格高度 layout: `total, sizes, prev, next, jumper`, //翻页器显示控件 // 列配置项 tableItems: [ { prop: "ID", label: "工号", minWidth: "50", mainKey: true }, //设置主键 { prop: "XM", label: "姓名", minWidth: "50", filterKey_end: true },//设置右侧穿梭表过滤关键词字段 { prop: "YHM", label: "用户名", minWidth: "50" }, ], tableData: [], //表格显示内容 startPage: { total: 0 }, //实际总数 }, // 渲染数据 tableData: [], tableData_bk: [], userList: [ { key: 1, label: "梁冰露" }, { key: 2, label: "吴梵听" }, { key: 3, label: "卢令美" }, { key: 4, label: "韩宛曼" }, { key: 5, label: "郝海冬" }, { key: 6, label: "傅优悦" }, { key: 7, label: "郝幻莲" }, { key: 8, label: "江嘉云" }, { key: 9, label: "梁秋芳" }, { key: 10, label: "郝悦颖" }, { key: 11, label: "廖芝蓉" }, { key: 12, label: "胡傲丝" }, { key: 13, label: "赵珺琦" }, { key: 14, label: "石心诺" }, { key: 15, label: "丁翠芙" }, { key: 16, label: "李夏河" }, { key: 17, label: "范水悦" }, { key: 18, label: "郑凝雪" }, { key: 19, label: "李亦玉" }, { key: 20, label: "袁三春" }, { key: 21, label: "赵红叶" }, { key: 22, label: "曹安琪" }, { key: 23, label: "谭琴音" }, { key: 24, label: "钟湛蓝" }, { key: 25, label: "陆之柔" }, { key: 26, label: "吕孒凡" }, { key: 27, label: "熊野雪" }, { key: 28, label: "曹叶澜" }, { key: 29, label: "韩粟梅" }, { key: 30, label: "孔杏儿" }, { key: 31, label: "宋若彤" }, { key: 32, label: "于淼淼" }, { key: 33, label: "潘欣跃" }, { key: 34, label: "石雅辰" }, { key: 35, label: "白念珍" }, { key: 36, label: "文爱茹" }, { key: 37, label: "王如曼" }, { key: 38, label: "宋丝琪" }, { key: 39, label: "王凝荷" }, { key: 40, label: "郑雨雪" }, { key: 41, label: "梁映阳" }, { key: 42, label: "徐新雨" }, { key: 43, label: "毛恬雅" }, { key: 44, label: "侯若蕊" }, { key: 45, label: "杨云蔚" }, { key: 46, label: "史之卉" }, { key: 47, label: "胡千束" }, { key: 48, label: "冯冷荷" }, { key: 49, label: "金语心" }, { key: 50, label: "江恬默" }, { key: 51, label: "高香馨" }, { key: 52, label: "江凌晴" }, { key: 53, label: "梁列琴" }, { key: 54, label: "邹鸾瑶" }, { key: 55, label: "夏素洁" }, { key: 56, label: "范秋玉" }, { key: 57, label: "钟北嘉" }, { key: 58, label: "谭水云" }, { key: 59, label: "顾山柏" }, { key: 60, label: "龙曼蔓" }, { key: 61, label: "钟双儿" }, { key: 62, label: "林林娜" }, { key: 63, label: "邹溪儿" }, { key: 64, label: "顾妙彤" }, { key: 65, label: "傅茵茵" }, { key: 66, label: "卢念露" }, { key: 67, label: "罗冷亦" }, { key: 68, label: "胡秋颖" }, { key: 69, label: "姜怡月" }, { key: 70, label: "傅和暄" }, { key: 71, label: "赖布凡" }, { key: 72, label: "郝念蕾" }, { key: 73, label: "邱天欣" }, { key: 74, label: "汤莉莉" }, { key: 75, label: "段靖易" }, { key: 76, label: "周之云" }, { key: 77, label: "董映秋" }, { key: 78, label: "汤玲琅" }, { key: 79, label: "田雁梅" }, { key: 80, label: "石雨雪" }, { key: 81, label: "任君雅" }, { key: 82, label: "蔡小谷" }, { key: 83, label: "孟忆之" }, { key: 84, label: "姜闲丽" }, { key: 85, label: "文忆香" }, { key: 86, label: "戴运虹" }, { key: 87, label: "王玄穆" }, { key: 88, label: "刘绿柳" }, { key: 89, label: "萧梦丝" }, { key: 90, label: "谭忆山" }, { key: 91, label: "方榕嫣" }, { key: 92, label: "徐欣合" }, { key: 93, label: "夏雨南" }, { key: 94, label: "尹沙羽" }, { key: 95, label: "万梦玉" }, { key: 96, label: "谢灵枫" }, { key: 97, label: "曾源源" }, { key: 98, label: "赖谷枫" }, { key: 99, label: "彭子童" }, ], }; }, created() { this.createTableData(); }, methods: { // 初始化、翻页、切换每页显示数量的时候触发 initTransfer({ keyword = "", currentPage = 1, pageSize = 10 } = {}) { // 模拟接口调用---------------------------------------- let results = this.tableData_bk.filter((obj) => keyword ? Object.keys(obj).some((k) => obj[k] .toString() .toLocaleLowerCase() .includes(keyword.toString().toLocaleLowerCase()) ) : true ); this.transferData.startPage.total = results.length; this.transferData.tableData = results.slice( (currentPage - 1) * pageSize, currentPage * pageSize ); // ---------------------------------------- }, // 构建数据 createTableData(d) { this.tableData_bk = this.userList.map((v) => { let ID = this.$g.getRandomID(); return { ID, XM: v.label, YHM: `user${ID}` }; }); this.initTransfer(); }, }, }; </script>