文章目录
概要
1. 本节主要是再次对已封装的组件进行运用示范。
2. 补充了表格高度会根据窗口自适应填满的功能
3. 补充了鼠标拖拽范围选取功能,可根据拖拽范围进行粘贴剪贴板中的数据到表格中。支持ctrl+v,excel的数据或任意单文本
4. 补充了以鼠标选中单元格为起始点进行数据赋值的功能。支持ctrl+v
示例动图
单表格
双表格框选赋值互不影响
一、组件应用
<template>
<div>
<!-- 筛选表单 -->
<div class="search-group">
<vxe-input
size="small"
placeholder="搜索"></vxe-input>
</div>
<!-- 表格上方按钮组 -->
<div class="list-button-area">
<vxe-toolbar>
<template #buttons>
<vxe-button @click="Enable()">启用禁用标拖拽选取</vxe-button>
</template>
</vxe-toolbar>
</div>
<custom-vxe-table
ref="xTable"
:dynamicHeight="600"
startActivatedPaste
:checkboxConfig="{
range: true
}"
showFooter
:summaryColKeyList="[
['num1', 'rate'],
['age', 'rate'],
]"
:sumConfig="[
{ columnIndex: 1, text: '合计(元)' },
{ columnIndex: 1, text: '平均值(元)', ruleName: 'avg' },
]"
:table-data="tableData">
<vxe-column
type="checkbox"
width="60"></vxe-column>
<vxe-column
type="seq"
width="150"></vxe-column>
<vxe-column
field="name"
title="name"
:edit-render="{ name: 'VxeInput' }"></vxe-column>
<vxe-column
field="num1"
title="num1"
:edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input
v-model="row.num1"
type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column
field="age"
:edit-render="{}">
<template #edit="{ row }">
<vxe-input
v-model="row.num1"
type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column
field="rate"
title="rate"></vxe-column>
</custom-vxe-table>
<el-divider>分割线</el-divider>
<custom-vxe-table
ref="xTable2"
:dynamicHeight="600"
showFooter
:summaryColKeyList="[
['num1', 'rate'],
['age', 'rate'],
]"
:sumConfig="[
{ columnIndex: 1, text: '合计(元)' },
{ columnIndex: 1, text: '平均值(元)', ruleName: 'avg' },
]"
:table-data="tableData2">
<vxe-column
type="checkbox"
width="60"></vxe-column>
<vxe-column
type="seq"
width="150"></vxe-column>
<vxe-column
field="name"
title="name"
:edit-render="{ name: 'VxeInput' }"></vxe-column>
<vxe-column
field="num1"
title="num1"
:edit-render="{}">
<template #edit="{ row }">
<vxe-input
v-model="row.num1"
type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column
field="age"
title="age"></vxe-column>
<vxe-column
field="rate"
title="rate"></vxe-column>
</custom-vxe-table>
</div>
</template>
<script>
import CustomVxeTable from '../components/custom-vxe-table'
// name > components > mixins > props > data > computed > watch > filter
export default {
name: 'demo-1', //
components: { CustomVxeTable },
data() {
return {
tableData: [
{
id: 10001,
name: 'Test1',
nickname: 'T1',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 40,
age: 28.2,
rate: 22,
},
{
id: 10002,
name: 'Test2',
nickname: 'T2',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10003,
name: 'Test3',
nickname: 'T3',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 10004,
name: 'Test4',
nickname: 'T4',
role: 'Designer',
sex: '1',
sex2: ['1'],
num1: 30,
age: 23.3,
rate: 13,
},
{
id: 10005,
name: 'Test5',
nickname: 'T5',
role: 'Develop',
sex: '0',
sex2: ['1', '0'],
num1: 20,
age: 30.01,
rate: 6,
},
{
id: 10006,
name: 'Test6',
nickname: 'T6',
role: 'Designer',
sex: '1',
sex2: ['0'],
num1: 10,
age: 21.03,
rate: 33,
},
{
id: 10007,
name: 'Test7',
nickname: 'T7',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 5,
age: 29,
rate: 4,
},
{
id: 10008,
name: 'Test8',
nickname: 'T8',
role: 'PM',
sex: '1',
sex2: ['0'],
num1: 2,
age: 35,
rate: 55,
},
{
id: 10009,
name: 'Test9',
nickname: 'T9',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 40,
age: 28.2,
rate: 22,
},
{
id: 10010,
name: 'Test10',
nickname: 'T10',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10011,
name: 'Test11',
nickname: 'T11',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 10012,
name: 'Test12',
nickname: 'T12',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10013,
name: 'Test13',
nickname: 'T13',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 100014,
name: 'Test14',
nickname: 'T14',
role: 'Designer',
sex: '1',
sex2: ['1'],
num1: 30,
age: 23.3,
rate: 13,
},
{
id: 100015,
name: 'Test15',
nickname: 'T15',
role: 'Develop',
sex: '0',
sex2: ['1', '0'],
num1: 20,
age: 30.01,
rate: 6,
},
{
id: 100016,
name: 'Test16',
nickname: 'T16',
role: 'Designer',
sex: '1',
sex2: ['0'],
num1: 10,
age: 21.03,
rate: 33,
},
{
id: 100017,
name: 'Test17',
nickname: 'T17',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 5,
age: 29,
rate: 4,
},
{
id: 100018,
name: 'Test18',
nickname: 'T18',
role: 'PM',
sex: '1',
sex2: ['0'],
num1: 2,
age: 35,
rate: 55,
},
],
tableData2: [
{
id: 10001,
name: 'Test1',
nickname: 'T1',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 40,
age: 28.2,
rate: 22,
},
{
id: 10002,
name: 'Test2',
nickname: 'T2',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10003,
name: 'Test3',
nickname: 'T3',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 10004,
name: 'Test4',
nickname: 'T4',
role: 'Designer',
sex: '1',
sex2: ['1'],
num1: 30,
age: 23.3,
rate: 13,
},
{
id: 10005,
name: 'Test5',
nickname: 'T5',
role: 'Develop',
sex: '0',
sex2: ['1', '0'],
num1: 20,
age: 30.01,
rate: 6,
},
{
id: 10006,
name: 'Test6',
nickname: 'T6',
role: 'Designer',
sex: '1',
sex2: ['0'],
num1: 10,
age: 21.03,
rate: 33,
},
{
id: 10007,
name: 'Test7',
nickname: 'T7',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 5,
age: 29,
rate: 4,
},
{
id: 10008,
name: 'Test8',
nickname: 'T8',
role: 'PM',
sex: '1',
sex2: ['0'],
num1: 2,
age: 35,
rate: 55,
},
{
id: 10009,
name: 'Test9',
nickname: 'T9',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 40,
age: 28.2,
rate: 22,
},
{
id: 10010,
name: 'Test10',
nickname: 'T10',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10011,
name: 'Test11',
nickname: 'T11',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 10012,
name: 'Test12',
nickname: 'T12',
role: 'Designer',
sex: '1',
sex2: ['0', '1'],
num1: 23,
age: 22.1,
rate: 34,
},
{
id: 10013,
name: 'Test13',
nickname: 'T13',
role: 'Test',
sex: '0',
sex2: ['1'],
num1: 200,
age: 32.1,
rate: 18,
},
{
id: 100014,
name: 'Test14',
nickname: 'T14',
role: 'Designer',
sex: '1',
sex2: ['1'],
num1: 30,
age: 23.3,
rate: 13,
},
{
id: 100015,
name: 'Test15',
nickname: 'T15',
role: 'Develop',
sex: '0',
sex2: ['1', '0'],
num1: 20,
age: 30.01,
rate: 6,
},
{
id: 100016,
name: 'Test16',
nickname: 'T16',
role: 'Designer',
sex: '1',
sex2: ['0'],
num1: 10,
age: 21.03,
rate: 33,
},
{
id: 100017,
name: 'Test17',
nickname: 'T17',
role: 'Develop',
sex: '0',
sex2: ['0'],
num1: 5,
age: 29,
rate: 4,
},
{
id: 100018,
name: 'Test18',
nickname: 'T18',
role: 'PM',
sex: '1',
sex2: ['0'],
num1: 2,
age: 35,
rate: 55,
},
],
}
},
mounted() {
// 注册单元格 鼠标拖拽范围选取功能
this.$refs.xTable.selectCell({ dom: this.$refs.xTable })
// this.$refs.xTable2.selectCell({ dom: this.$refs.xTable2 })
},
methods:{
Enable(){
this.$refs.xTable.selectCellToggle()
// this.$refs.xTable2.selectCellToggle()
}
}
}
</script>
二、注册拖拽选取事件
1.注册单元格 鼠标拖拽范围选取功能
// dom: 框选识别的范围
selectCell({ dom: this.$refs.xTable })
2.以鼠标选中单元格为起始点进行数据赋值
组件上开启 startActivatedPaste 参数
上面双表格例子中,第一个表格开启了这个参数,第二个没有开启。所以第一个表格支持选中单元格直接粘贴数据
三、功能代码说明
1.表格组件代码更新
src\components\custom-vxe-table\index.vue
<template>
<div class="table-box">
<vxe-table
ref="vxetablem"
v-on="$listeners"
v-bind="$attrs"
:loading="loading"
:loading-config="loadingOpts"
:auto-resize="autoResize"
:stripe="stripe"
:align="align"
:header-align="headerAlign"
:merge-cells="mergeCells"
:tree-config="treeOpts"
:radio-config="radioOpts"
:checkbox-config="checkboxOpts"
:show-overflow="showOverflow"
:show-header-overflow="showHeaderOverflow"
:show-footer-overflow="showFooterOverflow"
:keep-source="keepSource"
:border="border"
:height="height"
:min-height="minHeight"
:max-height="maxHeight"
:data="tData"
:show-footer="showFooter"
:column-config="columnOpts"
:resize-config="resizeOpts"
:row-config="rowOpts"
:menu-config="ctxMenuOpts"
:edit-config="editOpts"
:expand-config="expandConfig"
:tooltip-config="tooltipOpts"
:span-method="spanMethod"
:row-class-name="rowClassName"
:cell-class-name="cellClassName"
:header-row-class-name="headerRowClassName"
:header-cell-class-name="headerCellClassName"
:footer-row-class-name="footerRowClassName"
:footer-cell-class-name="footerCellClassName"
:footer-data="footerData"
:footer-align="footerAlign"
:footer-span-method="footerSpanMethod"
:footer-method="footerMethod"
:cell-style="cellStyle"
@header-cell-click="$emit('headerCellClick')"
@header-cell-dblclick="$emit('headerCellDblclick')"
@header-cell-menu="$emit('headerCellMenu')"
@cell-click="$emit('cellClick')"
@edit-activated="handleEditActivated"
@cell-dblclick="$emit('cellDBLClick')"
@cell-mouseenter="$emit('cellMouseenter')"
@cell-mouseleave="$emit('cellMouseleave')"
@cell-menu="$emit('cellMenu')"
@radio-change="$emit('radioChange')"
@checkbox-change="checkboxChange"
@checkbox-all="checkboxAll"
@scroll="$emit('scroll')">
<slot></slot>
</vxe-table>
</div>
</template>
<script>
/* global XEUtils:false */
/* ==================== 说明开始 ==================== */
// 1. API详见 https://vxetable.cn/v3.8/#/table/api
// 2. 该表格属性若干,该版只重定义了部分常用属性,其它方法可直接在组件上按官网案例调用,其方法和属性穿透实现方式为:$listeners、$attrs
// 3. 公用属性已提取到同级conf.js中,补充属性请按此规则配置并补充对应参数说明
`表格底部求和及平局值调用示例:
<custom-vxe-table
showFooter
:summaryColKeyList="[['classify','allSum'],['allSum','agreementMoney']]"
:sumConfig="[{columnIndex: 1, text: '合计(元)' },{columnIndex: 1, text: '平均值(元)', ruleName: 'avg' }]"
:table-data="data"
@selection-change="handleSelectionChange">
<vxe-column type="seq" width="50"></vxe-column>
<vxe-column field="classify" title="classify"></vxe-column>
<vxe-column field="allSum" title="allSum"></vxe-column>
<vxe-column field="agreementMoney" title="agreementMoney"></vxe-column>
</custom-vxe-table>`
/* ==================== 说明结束 ==================== */
import XEUtils from 'xe-utils'
import GlobalConfig from './conf'
import { getTableHeight } from '@/utils/tablebodyheight'
import { validatenull } from '@/utils/validate'
import { selectionarea } from '@/mixins/selection-area'
export default {
name: 'vxe-table-m',
mixins: [selectionarea],
props: {
tableData: {
type: Array,
default: () => [],
},
// tableHeight、minHeight、maxHeight 三者之间的关系见 https://www.cnblogs.com/xldbk/p/12114686.html
tableHeight: {
type: [Number, String],
},
minHeight: { type: [Number, String], default: () => GlobalConfig.table.minHeight },
maxHeight: {
type: [Number, String],
default: () => GlobalConfig.table.maxHeight,
},
// 最终高度需要再减去的高度,常用来减去已知的自定义元素高度,此数值需要是1920*1080分辨率下的像素值(只有不传入其它任何高度限制时生效)
dynamicHeight: {
type: [Number, String],
default: 0,
},
// 保持原始值的状态,被某些功能所依赖,比如编辑状态、还原数据等(开启后影响性能,具体取决于数据量)
keepSource: { type: Boolean, default: () => GlobalConfig.table.keepSource },
// 是否自动监听父容器变化去更新响应式表格宽高
autoResize: {
type: Boolean,
default: () => {
return false
},
},
// 响应式布局配置项
resizeConfig: {
type: Object,
},
// 是否带有斑马纹
stripe: { type: Boolean, default: () => GlobalConfig.table.stripe },
border: { type: [Boolean, String], default: () => GlobalConfig.table.border },
loading: {
type: Boolean,
default: () => {
return false
},
},
loadingConfig: {
type: Object,
},
// 所有的列对其方式
align: { type: String, default: () => GlobalConfig.table.align },
// 所有的表头列的对齐方式
headerAlign: { type: String, default: () => GlobalConfig.table.headerAlign },
// 所有的表尾列的对齐方式
footerAlign: { type: String, default: () => GlobalConfig.table.footerAlign },
// 给行附加 className
rowClassName: {
type: [String, Function],
default: () => {
return ''
},
},
// 给单元格附加 className
cellClassName: [String, Function],
// 给表头的行附加 className
headerRowClassName: [String, Function],
// 给表头的单元格附加 className
headerCellClassName: [String, Function],
rowConfig: {
type: Object,
},
// 列默认配置项
columnConfig: {
type: Object,
default: () => GlobalConfig.table.columnConfig,
},
// 快捷菜单配置项(右键)
menuConfig: {
type: Object,
},
// 展开行配置项
expandConfig: {
type: Object,
},
// 编辑配置项
editConfig: {
type: Object,
},
// 合并指定单元格
mergeCells: {
type: Array,
},
tooltipConfig: {
type: Object,
},
// 树形结构配置项
treeConfig: [Boolean, Object],
radioConfig: {
type: Object,
},
checkboxConfig: {
type: Object,
},
// 设置所有内容过长时显示为省略号
showOverflow: { type: [Boolean, String], default: () => GlobalConfig.table.showOverflow },
// 设置表头所有内容过长时显示为省略号
showHeaderOverflow: {
type: [Boolean, String],
default: () => GlobalConfig.table.showHeaderOverflow,
},
// 设置表尾所有内容过长时显示为省略号
showFooterOverflow: {
type: [Boolean, String],
default: () => GlobalConfig.table.showFooterOverflow,
},
// 是否显示表尾合计
showFooter: {
type: Boolean,
},
// 表尾数据
footerData: {
type: Array,
},
// 自定义合并行或列的方法
spanMethod: Function,
// 表尾合并行或列
footerSpanMethod: Function,
// 给表尾的行附加 className
footerRowClassName: [String, Function],
// 给表尾的单元格附加 className
footerCellClassName: [String, Function],
// 给单元格附加样式
cellStyle: [Object, Function],
// 需要计算合计的列的key, 二维数组, key可以是无序的 [['第一行key1', '第一行key2'],['第二行key1','第二行key2']], 如果只传一个数组,后续计算则复用
summaryColKeyList: Array,
/**
* 合计描述及计算规则, 对应footerMethod方法的返回值(多行合计数组),即:
* [{columnIndex: '第一行合计行描述列下标', text: '描述', ruleName: '计算规则(求和)'},{columnIndex: '第二行合计行描述列下标',text: '描述', ruleName: '计算规则(平均值)'}]
* 计算规则参数(ruleName): add 求和, avg 平均值 , 默认值:add。结果保留2位小数
*/
sumConfig: {
type: Array,
required: true,
default: () => {
return [{ columnIndex: 1, text: '合计(元)', ruleName: 'add' }]
},
},
/**
* 如果不想自动求和,比如直接使用后端请求回来的数据,则需要手动设置summaryData。此时将不启用sumConfig中的(ruleName)计算规则进行计算
* 对象数组(对应summaryColKeyList中的key) [{第一行key1:'第一行key1的值', 第一行key2:'第一行key2的值'},...]
*/
summaryData: Array,
// 是否开启粘贴功能,已选择的单元格为起点
startActivatedPaste:{
type: Boolean,
}
},
data() {
return {
height: '', // 表格高度
}
},
watch: {
tableData: {
handler(newValue) {
let _self = this
if (!validatenull(newValue)) {
_self.$nextTick(() => {
// 官方的展开 只有再数据初始化时才生效且只执行一次,而在表格嵌套在tab中且懒加载数据时是不会再触发展开功能的。
// 所以这里手动触发一下,但也只触发一次。expandInit: false 表示只触发一次
if (!_self.treeOpts?.expandInit && _self.treeConfig?.expandAll) {
_self.treeOpts.expandInit = true
_self.$refs['vxetablem'].setAllTreeExpand(true)
}
})
}
},
immediate: true,
deep: true,
},
},
computed: {
tData() {
if (!validatenull(this.tableData)) {
this.autoHeight()
}
return this.tableData
},
columnOpts() {
return Object.assign({}, GlobalConfig.table.columnConfig, this.columnConfig)
},
rowOpts() {
return Object.assign({}, GlobalConfig.table.rowConfig, this.rowConfig)
},
resizeOpts() {
return Object.assign({}, GlobalConfig.table.resizeConfig, this.resizeConfig)
},
radioOpts() {
return Object.assign({}, GlobalConfig.table.radioConfig, this.radioConfig)
},
checkboxOpts() {
return Object.assign({}, GlobalConfig.table.checkboxConfig, this.checkboxConfig)
},
tooltipOpts() {
return Object.assign({}, GlobalConfig.table.tooltipConfig, this.tooltipConfig)
},
editOpts() {
return Object.assign({}, GlobalConfig.table.editConfig, this.editConfig)
},
treeOpts() {
return Object.assign({}, GlobalConfig.table.treeConfig, this.treeConfig)
},
ctxMenuOpts() {
return Object.assign({}, GlobalConfig.table.menuConfig, this.menuConfig)
},
loadingOpts() {
return Object.assign({}, GlobalConfig.table.loadingConfig, this.loadingConfig)
},
},
mounted() {
let _self = this
_self.autoHeight()
// 监听页面缩放, 重新计算表格高度
window.onresize = XEUtils.debounce(() => {
return (() => {
// _self.autoHeight()
})()
}, 200)
},
methods: {
// 表尾合计,
footerMethod({ columns, data }) {
let _self = this
if (!_self.showFooter) return
let sumRows = []
if (!validatenull(_self.sumConfig)) {
_self.sumConfig.forEach((conf, index) => {
sumRows.push(
columns.map((column, columnIndex) => {
// 设置表尾合计的描述
if (columnIndex == conf.columnIndex) {
return conf.text
}
// 对符合要求的列进行计算
if (!validatenull(_self.summaryColKeyList)) {
let keyList = _self.summaryColKeyList[index]
? _self.summaryColKeyList[index]
: _self.summaryColKeyList[0]
if (!validatenull(keyList) && keyList?.includes(column.field)) {
if (!validatenull(_self.summaryData?.[index])) {
return _self.summaryData[index][column.field] || null
}
return _self.sumNum(data, column.field, conf.ruleName || 'add')
}
}
return null
})
)
})
}
return sumRows
},
// 根据sumConfig中配置的计算规则进行计算
sumNum(list, field, rule) {
let count = 0
if (rule == 'add') {
list.forEach((item) => {
count += Number(item[field])
})
return Math.round(count * 100) / 100
}
if (rule == 'avg') {
// 求平均...
list.forEach((item) => {
count += Number(item[field])
})
return Math.round((count / list.length) * 100) / 100
}
},
// checkbox选中状态变换. 兼容el-table
checkboxChange(params) {
let selections = params.records || []
let selectionIds = selections.map((item) => {
return item.id
})
this.$emit('selection-change', selections, selectionIds, params)
},
// 计算table页面高度: 如果传入自定义高度,则使用自定义高度,否则动态计算高度
autoHeight() {
this.$nextTick(() => {
if (this.maxHeight == null) {
if (this.tableHeight) {
this.height = this.tableHeight
} else {
this.height = getTableHeight({
dynamicHeight: this.dynamicHeight,
showPagination: this.tableData.length > 0,
})
}
}
})
},
// checkbox全选状态变换. 兼容el-table
checkboxAll(params) {
let selections = params.records || []
this.$emit('select-all', selections, params)
// 兼容el-table 触发逻辑,select-all会联动selection-change
let selectionIds = selections.map((item) => {
return item.id
})
this.$emit('selection-change', selections, selectionIds, params)
},
},
}
</script>
<style lang="scss" scoped>
.table-box {
background-color: #fff;
}
</style>
补充了表格动态计算代码
import { getTableHeight } from ‘@/utils/tablebodyheight’
补充了一个混入函数
import { selectionarea } from ‘@/mixins/selection-area’
2. tablebodyheight.js
src\utils\tablebodyheight.js
import { validatenull } from '@/utils/validate'
/**
* 动态计算表格高度
* 表格最终高度 = 屏幕可视区域高度 - tabs表头的高度(现在支持两层tabs嵌套) - 搜索条件(search-group) - 表格上方按钮(list-button-area) - 底部的分页(pagination-container) - 及以上提及dom所使用的上下外边距
* @param {*} dynamicHeight 分页页面内容中如果有已知高度的元素,将1920*1080下的像素值高度传入,自动计算其他分辨率下的高度,并减掉,返回的是净高度。
* @param {*} showPagination 页面中是否显示了分页组件
*/
export function getTableHeight({ selectorName = 'body', dynamicHeight, showPagination }) {
let defaultDom = document.createElement('div')
// 默认10行数据高度(0.21875rem=42px), 假设每行高度在1920*1080分辨率下为42px
let tableRowheight = pxToRem(0.21875) * 10
// 页面高度
let contentDom = document.querySelector(selectorName)
let mainH = getComputedStyle(contentDom || defaultDom)?.height?.replace('px', '') || 0
console.log('%c [ mainH ]-16', 'font-size:13px; background:#a31039; color:#e7547d;', mainH)
// 最外层tabs
let outerLayerTabs = contentDom.querySelector('.el-tabs .el-tabs__header')
let outerLayerTabsHeaderH = 0
let outerLayerTabsHeaderMarginBottom = 0
if (!validatenull(outerLayerTabs)) {
let tabsComputed = getComputedStyle(outerLayerTabs) || defaultDom
if (tabsComputed?.height == 'auto' || validatenull(tabsComputed?.height)) {
outerLayerTabsHeaderH = 0
} else {
outerLayerTabsHeaderH = tabsComputed?.height?.replace('px', '')
}
outerLayerTabsHeaderMarginBottom = tabsComputed?.marginBottom?.replace('px', '') || 0
}
// tab中嵌套tabs的情况(第二层tabs)
// let childTabsContentDom = contentDom.querySelector('.child-tabs')
// if (childTabsContentDom && childTabsContentDom.querySelector('.el-tabs__content')) {
// let tabPaneDom = childTabsContentDom.querySelector('.el-tabs__content').childNodes
// for (let t = 0; t < tabPaneDom.length; t++) {
// const item = tabPaneDom[t]
// if (item.hasAttribute && !item.hasAttribute('aria-hidden')) {
// contentDom = item
// break
// }
// }
// }
// 表格内容区域 底部内边距
let tableBox = contentDom.querySelector('.table-box')
let tableBoxPaddingBottom = getComputedStyle(tableBox || defaultDom)?.paddingBottom?.replace('px', '') || 0
// 分页
let paginationDom = contentDom.querySelector('.pagination-container')
let paginationH = getComputedStyle(paginationDom || defaultDom)?.height
// auto作用为需要分页功能但是隐藏状态
if (paginationDom?.style?.display == 'none' || paginationH == 'auto') {
paginationH = showPagination ? remToPx(55 / 192) : 0 // 55为分页默认高度
} else {
paginationH = paginationH.replace('px', '')
}
// 模拟分页组件底部边距,15px
let pageBottomMarginBottom = remToPx(15 / 192)
let height =
Number(mainH) -
Number(outerLayerTabsHeaderH) -
Number(outerLayerTabsHeaderMarginBottom) -
Number(tableBoxPaddingBottom) -
Number(paginationH) -
Number(pageBottomMarginBottom)
// let dynamicHeightToRem_192 = pxToRem(dynamicHeight, 192)
// let dynamicHeightToPX_rule = remToPx(dynamicHeightToRem_192) || 0
// height = dynamicHeight ? height - dynamicHeightToPX_rule : height
height = dynamicHeight ? height - dynamicHeight : height
height = height > tableRowheight ? height : tableRowheight
return height
}
// px转rem, rem = px / 转换基数
export function pxToRem(px = 0, rootV) {
let defaultDom = document.createElement('div')
// 获取屏幕px和rem转换基数
let rootValue =
getComputedStyle(document.getElementsByTagName('html')[0] || defaultDom).fontSize?.replace('px', '') || 192
return Number(px) / (rootV ? rootV : rootValue)
}
// rem转px, px = rem * 转换基数
export function remToPx(rem = 0, rootV) {
let defaultDom = document.createElement('div')
// 获取屏幕px和rem转换基数
let rootValue =
getComputedStyle(document.getElementsByTagName('html')[0] || defaultDom).fontSize?.replace('px', '') || 192
return Number(rem) * (rootV ? rootV : rootValue)
}
// 数字金额转大写
export const numToCny = (money) => {
// 汉字的数字
let cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
// 基本单位
let cnIntRadice = ['', '拾', '佰', '仟']
// 对应整数部分扩展单位
let cnIntUnits = ['', '万', '亿', '兆']
// 对应小数部分单位
let cnDecUnits = ['角', '分', '毫', '厘']
// 整数金额时后面跟的字符
let cnInteger = '整'
// 整型完以后的单位
let cnIntLast = '元'
// 最大处理的数字
let maxNum = 999999999999999.9999
// 金额整数部分
let integerNum
// 负数
let negative = '负'
// 负数
let negativeFlag = false
// 金额小数部分
let decimalNum
// 输出的中文金额字符串
let chineseStr = ''
// 分离金额后用的数组,预定义
let parts
if (money == 0) {
chineseStr = cnNums[0] + cnIntLast + cnInteger
return chineseStr
}
if (money < 0) {
money = 0 - money
negativeFlag = true
}
if (money == '' || money == null || money == undefined) {
return ''
}
money = parseFloat(money)
if (money >= maxNum) {
// 超出最大处理数字
return ''
}
// 转换为字符串
money = money.toString()
if (money.indexOf('.') == -1) {
integerNum = money
decimalNum = ''
} else {
parts = money.split('.')
integerNum = parts[0]
decimalNum = parts[1].substr(0, 4)
}
// 获取整型部分转换
if (parseInt(integerNum, 10) > 0) {
let zeroCount = 0
let IntLen = integerNum.length
for (let i = 0; i < IntLen; i++) {
let n = integerNum.substr(i, 1)
let p = IntLen - i - 1
let q = p / 4
let m = p % 4
if (n == '0') {
zeroCount++
} else {
if (zeroCount > 0) {
chineseStr += cnNums[0]
}
// 归零
zeroCount = 0
chineseStr += cnNums[parseInt(n)] + cnIntRadice[m]
}
if (m == 0 && zeroCount < 4) {
chineseStr += cnIntUnits[q]
}
}
chineseStr += cnIntLast
}
// 小数部分
if (decimalNum != '') {
let decLen = decimalNum.length
for (let j = 0; j < decLen; j++) {
let x = decimalNum.substr(j, 1)
if (x != '0') {
chineseStr += cnNums[Number(x)] + cnDecUnits[j]
}
}
}
if (chineseStr == '') {
chineseStr += cnNums[0] + cnIntLast + cnInteger
} else if (decimalNum == '') {
chineseStr += cnInteger
}
if (negativeFlag) {
return negative + chineseStr
}
return chineseStr
}
3. selection-area.js
src\mixins\selection-area.js
默认情况下,鼠标范围框选是不启用的。如果要默认启用
直接在文件中删除该代码
// 设置默认禁用状态
_self.selection.disable()
/* global XEUtils:false */
/* global VXETable:false */
import SelectionArea from '@simonwep/selection-js'
import XEUtils from 'xe-utils'
import { Message, MessageBox } from 'element-ui'
import { validatenull } from '@/utils/validate'
// 表格拖拽范围选择
export const selectionarea = {
data() {
return {
selection: null,
selectedCell: [],
selectCellStatus: true,
}
},
methods: {
// 启用禁用标拖拽选取-范围赋值
selectCellToggle() {
let _self = this
const $grid = _self.$refs.vxetablem
if (_self.selectCellStatus) {
_self.selection.enable()
_self.listenerPasteAreaCell()
} else {
document.removeEventListener('paste', this.listenerPasteAreaFunc)
_self.selection.disable()
_self.clearCellActiveClass($grid)
}
_self.selectCellStatus = !_self.selectCellStatus
},
// 单元格鼠标拖拽选取
selectCell({ dom }) {
let _self = this
let firstChild = dom?.$el?.firstChild
if (firstChild) {
let tid = firstChild.classList.values().find((item) => {
return item.indexOf('tid_') > -1
})
if (validatenull(tid)) return
let cont = '.vxe-table.' + tid
let body = cont + ' .vxe-table--body'
let bodyTd = body + ' td'
_self.selection = new SelectionArea({
// document object - if you want to use it within an embed document (or iframe).
// document: dom,
class: 'selection-area',
container: cont, //'.vxe-table--body', // 查询选择器或 dom-node 为 selection-area 元素设置容器。
selectables: [bodyTd], // 查询可以选择的元素的选择器。
startareas: [body], // 查询元素的选择器,从中可以开始选择。
boundaries: [body], // 查询将用作所选内容边界的元素的选择器。
// px, how many pixels the point should move before starting the selection (combined distance).
// Or specifiy the threshold for each axis by passing an object like {x: <number>, y: <number>}.
startThreshold: 20, // 移动多少像素px开始进行选择 {x: <number>, y: <number>}.
// Enable / disable touch support
allowTouch: true, // 触摸支持
// On which point an element should be selected.
// Available modes are cover (cover the entire element), center (touch the center) or
// the default mode is touch (just touching it).
intersect: 'touch', // 选中的时机:整个覆盖cover,移动到中心center, 触摸到touch
// Specifies what should be done if already selected elements get selected again.
// invert: Invert selection for elements which were already selected
// keep: Make stored elements (by keepSelection()) 'fix'
// drop: Remove stored elements after they have been touched
overlap: 'invert',
// Configuration in case a selectable gets just clicked
singleTap: {
// Enable single-click selection (Also disables range-selection via shift + ctrl).
allow: true,
// 'native' (element was mouse-event target) or 'touch' (element visually touched).
intersect: 'native',
},
scrolling: {
// On scrollable areas the number on px per frame is devided by this amount.
// Default is 10 to provide a enjoyable scroll experience.
speedDivider: 10,
// Browsers handle mouse-wheel events differently, this number will be used as
// numerator to calculate the mount of px while scrolling manually: manualScrollSpeed / scrollSpeedDivider.
manualSpeed: 750,
},
startScrollMargins: { x: 110, y: 110 },
})
_self.selection
.on('beforestart', (params) => {
let { event, store } = params
console.log('%c [ params ]-132', 'font-size:13px; background:#1ed0fe; color:#62ffff;', params)
event.preventDefault() // 拖拽时屏蔽选中文字
event.stopPropagation() // 阻止事件穿透,防止激活输入框
// return event.target.tagName !== "TD"; // 只识别TD标签
// return !event.path.some(item => {
// // item is in this case an element affected by the event-bubbeling.
// // To exclude elements with class "blocked" you could do the following (#73):
// return item.classList.contains('blocked');
// // If the areas you're using contains input elements you might want to prevent
// // any out-going selections from these elements (#72):
// return event.target.tagName !== 'INPUT';
// });
})
.on('start', (params) => {
let { event } = params
event.stopPropagation() // 阻止事件穿透,防止激活输入框
if (event.button != 2 && !event.ctrlKey && !event.metaKey) {
// const $grid = _self.$refs.vxetablem
// console.log('%c [ $grid ]-98', 'font-size:13px; background:#1858df; color:#5c9cff;', $grid)
_self.clearCellActiveClass(dom)
console.log('[ 清除 ] >')
}
})
.on(
'move',
({
store: {
changed: { added, removed },
},
event,
}) => {
// let h = _self.selection.h
if (event?.button === 0) {
addClass(added)
removeClass(removed)
}
}
)
.on('stop', ({ event }) => {
event.stopPropagation() // 阻止事件穿透,防止激活输入框
event.preventDefault() // 拖拽时屏蔽选中文字
if (event?.button === 0) {
// _self.selectedCell = store?.selected?.filter((item) => {
// return Array(...item.classList).includes('td-mouse-active')
// })
_self.selection.keepSelection()
}
})
}
// 设置默认禁用状态
_self.selection.disable()
// 设置单元格选中样式
function addClass(els) {
// console.log('%c [ els ]-453', 'font-size:13px; background:#326519; color:#76a95d;', els)
return els.map((v) => {
if (!v.classList.contains('td-mouse-active')) {
v.classList.add('td-mouse-active')
}
})
}
// 移除单元格选中样式
function removeClass(els) {
// console.log('%c [ els ]-462', 'font-size:13px; background:#a4963f; color:#e8da83;', els)
return els.map((v) => {
if (v.classList.contains('td-mouse-active')) {
v.classList.remove('td-mouse-active')
}
})
}
},
// 单元格被激活编辑时会触发该事件
handleEditActivated(params) {
this.$emit('edit-activated', params)
if (this.startActivatedPaste) {
this.ActivatCell = params
console.log('%c [ 激活的单元格 ]-163', 'font-size:13px; background:#8e669c; color:#d2aae0;', this.ActivatCell)
// ps 必须独立方法,便于移除监听
document.addEventListener('paste', this.listenerPasteStartFunc, false)
}
},
listenerPasteStartFunc(event) {
let _self = this
_self.pasteForCopeStartCell()
},
// 单元格被退出编辑时会触发该事件
handleEditClosed(params) {
document.removeEventListener('paste', this.listenerPasteStartFunc)
},
// 监听ctrl+v事件2
listenerPasteAreaCell() {
// ps 必须独立方法,便于移除监听
document.addEventListener('paste', this.listenerPasteAreaFunc)
},
listenerPasteAreaFunc(event) {
let _self = this
const $grid = _self.$refs.vxetablem
// 防止默认行为
event.preventDefault()
_self.pasteForCope($grid)
},
// 根据鼠标框选范围粘贴数据
pasteForCope($grid) {
let _self = this
// 根据框选范围开始赋值,横纵依次类推
let selectedCell = _self.selection.getSelection()
if (!validatenull(selectedCell)) {
// console.log('%c [ selected ]-298', 'font-size:13px; background:#2bb077; color:#6ff4bb;', _self.selectedCell)
// 获取剪贴板数据进行赋值。赋值规则为 从左到右 从上到下。如果剪贴板中只有一行一列的数据,则对所有已框选的单元格使用相同值进行赋值,同excel操作
// 数据量大的话还有效率优化空间, 建议:把框选的数据组织成和剪贴板一样的结构,遍历剪贴版数据进行赋值
_self.getClipboardData(({ data }) => {
if (!validatenull(data)) {
let { fullData } = $grid?.getTableData()
let fData = XEUtils.toTreeArray(fullData)
let cpRowIndex = -1
let cpColIndex = 0
let cpRow = []
let _selectRowIndex = ''
selectedCell.forEach((td, index) => {
const column = $grid.getColumnNode(td)
const field = column?.item?.field || ''
if (!validatenull(field)) {
const rowNode = $grid.getRowNode(td.parentNode)
let selectRowIndex = $grid.getVTRowIndex(rowNode.item)
if (selectRowIndex !== _selectRowIndex) {
_selectRowIndex = selectRowIndex
cpRowIndex++
cpColIndex = 0
cpRow = data?.[cpRowIndex] || []
}
let dataRow = fData[selectRowIndex]
if (!validatenull(cpRow[cpColIndex])) {
dataRow[field] = cpRow[cpColIndex]
cpColIndex++
} else {
return
}
}
})
_self.clearCellActiveClass($grid)
}
})
} else {
_self.$message.warning('请框选要粘贴的数据区域!')
}
},
// 以鼠标选中单元格做起始点粘贴数据
pasteForCopeStartCell() {
let _self = this
const { $columnIndex, rowIndex, $table } = _self.ActivatCell
const columns = $table.getColumns()
// 取消单元格编辑状态 全局
$table.clearEdit()
// 以选中单元格开始赋值,横纵依次类推
let startColumnIndex = $columnIndex
let startRowIndex = rowIndex
// 获取剪贴板数据进行赋值。赋值规则为 从左到右 从上到下。
_self.getClipboardData(({ data, sourceData, type }) => {
let contentText = []
if (type === 'text/html') {
contentText = [[sourceData.documentElement.textContent]]
} else if (type === 'text/plain') {
contentText = [[sourceData]]
}
let _data = data.length > 0 ? data : contentText
if (!validatenull(_data)) {
let { fullData } = $table?.getTableData()
let fData = XEUtils.toTreeArray(fullData)
_data.forEach((cpRow) => {
for (let i = 0; i < cpRow.length; i++) {
if (validatenull(columns[startColumnIndex]) || validatenull(fData[startRowIndex])) {
break
}
let field = columns[startColumnIndex]?.field ?? ''
fData[startRowIndex][field] = cpRow[i] ?? ''
startColumnIndex += 1
}
startColumnIndex = $columnIndex
startRowIndex += 1
})
}
})
},
// 清除选中单元格样式
clearCellActiveClass($grid) {
if ($grid) {
this.selection.clearSelection()
let tbody = $grid?.$el?.querySelector('.vxe-table--body-wrapper.body--wrapper')
let trs = tbody?.getElementsByTagName('tr')
for (var i = 0; i < trs.length; i++) {
let tr = trs[i]
let tds = tr.getElementsByTagName('td')
for (var j = 0; j < tds.length; j++) {
let td = tds[j]
if (td.classList.contains('td-mouse-active')) {
td.classList.remove('td-mouse-active')
}
}
}
}
},
/**
* 获取剪贴版数据
* @returns { data: Array }
*/
getClipboardData(callback) {
if (XEUtils.isFunction(callback)) {
this.insertForClipboard('', '', callback)
}
},
decidePromiseState(promise) {
const PROMISE_STATE = {
PENDING: 'pending',
FULFILLED: 'fulfilled', // 成功
REJECTED: 'rejected', // 失败
}
const t = {}
return Promise.race([promise, t])
.then((v) => (v === t ? PROMISE_STATE.PENDING : PROMISE_STATE.FULFILLED))
.catch(() => PROMISE_STATE.REJECTED)
},
// 剪切板内容 insertForClipboard(右键菜单实例,类型)
// type:从选中行向下插入行insert, 从选中单元格开始赋值pasteForCope, 如果传空则返回剪贴板数据
insertForClipboard(params, type, callback) {
let _self = this
const enterStr = '\r\n'
const spaceStr = '\t'
let clipboardRead = window.navigator.permissions.query({
name: 'clipboard-read',
})
if (!clipboardRead) return
clipboardRead.then((res) => {
if (res.state == 'denied') {
Message.error('不支持获取剪切板内容')
return
}
navigator.clipboard
.read()
.then(async (data) => {
let ps_html = 'rejected'
await _self.decidePromiseState(data[0].getType('text/html')).then((state) => {
ps_html = state
if (state === 'fulfilled') {
data[0].getType('text/html').then((res) => {
let reader = new FileReader()
//以下这两种方式都可以解析出来,因为Blob对象的数据可以按文本或二进制的格式进行读取
//reader.readAsBinaryString(blob);
reader.readAsText(res, 'utf8')
reader.onload = function () {
let fileTxt = this.result //这个就是解析出来的数据
let $doc = new DOMParser().parseFromString(fileTxt, 'text/html')
const $trs = Array.from($doc.querySelectorAll('table tr'))
if (!validatenull($trs)) {
// 解析剪贴板数据,生成行数据
let rowsInfo = []
$trs.forEach((tr) => {
let trData = []
if (!validatenull(tr.children)) {
let childrens = tr.children
for (let l = 0; l < childrens.length; l++) {
const td = childrens[l]
trData.push(td.textContent)
}
rowsInfo.push(trData)
}
})
if (!validatenull(rowsInfo)) {
// 插入
if (type == 'insert') {
_self.setRowData(params, rowsInfo)
}
// 粘贴
// if (type == 'pasteForCope') {
// pasteForCope(params, rowsInfo)
// }
if (XEUtils.isFunction(callback)) {
callback({ data: rowsInfo, sourceData: $doc, type: 'text/html' })
console.log('[ html ] >')
}
}
} else {
callback({ data: [], sourceData: $doc, type: 'text/html' })
// const bodyChild_div = Array.from($doc.querySelectorAll('body div'))
// const bodyChild_p = Array.from($doc.querySelectorAll('p'))
// const bodyChild_sp = Array.from($doc.querySelectorAll('span'))
// if (!validatenull(bodyChild_div) || !validatenull(bodyChild_p) || !validatenull(bodyChild_sp)) {
// Message.error('剪贴板数据不是表格(excel)格式!')
// } else {
// Message.error('剪切板内容为空或无法识别!')
// return
// }
}
}
})
}
})
if (ps_html == 'rejected') {
_self.decidePromiseState(data[0].getType('text/plain')).then((state) => {
if (state === 'fulfilled') {
data[0].getType('text/plain').then((res) => {
let reader = new FileReader()
//以下这两种方式都可以解析出来,因为Blob对象的数据可以按文本或二进制的格式进行读取
//reader.readAsBinaryString(blob);
reader.readAsText(res, 'utf8')
reader.onload = function () {
let fileTxt = this.result //这个就是解析出来的数据
let txtRows = fileTxt.split(enterStr)
let rowsInfo = []
txtRows.forEach((item) => {
let row = item.split(spaceStr)
rowsInfo.push(row)
})
if (!validatenull(rowsInfo)) {
// 插入
if (type == 'insert') {
_self.setRowData(params, rowsInfo)
}
// 粘贴
// if (type == 'pasteForCope') {
// pasteForCope(params, rowsInfo)
// }
if (XEUtils.isFunction(callback)) {
callback({ data: rowsInfo, sourceData: fileTxt, type: 'text/plain' })
console.log('[ plain ] >')
}
}
}
})
}
})
}
})
.catch((error) => {
Message.error('获取剪切板内容失败!')
console.error('Failed to read text from clipboard: ', error)
})
})
},
},
// deactivated() {
// document.removeEventListener('paste', this.listenerPasteStartFunc)
// document.removeEventListener('paste', this.listenerPasteAreaFunc)
// },
// beforeDestory() {
// this.selection?.destroy()
// },
}
4. 单元格选中样式及拖拽范围样式
src\styles\vxe-table-reset.scss
// 鼠标滑动范围框
.selection-area {
background: rgba(50, 117, 252, 0.11);
border: 1px solid rgba(46, 115, 252, 0.5);
border-radius: 0.1em;
user-select: none;
pointer-events: none;
}
// 单元格选中样式
.td-mouse-active {
background-color: rgba(64, 158, 255, 0.2);
border: 1px solid #409eff;
user-select: none;
z-index: 1;
& + .td-mouse-active {
border-left: none;
}
}
创建vxe-table-reset.scss文件后在main.js中引用
import “@/styles/vxe-table-reset.scss”;
插件版本说明
"vxe-table": "^3.8.11",
"xe-utils": "^3.5.12",
"element-ui": "^2.15.10",
"@simonwep/selection-js": "^2.1.2",
完结散花~
链接指引
💡 关于vxe-table的使用心得及扩展【表格虚拟滚动】(非插件)
💡💡 关于vxe-table的使用心得及扩展2【table表格二次封装】(非插件)