element ui 表格扩展,支持移动端自适应卡片式布局渲染
默认的elment ui的table组件,在移动端体验很差
所以需要扩展一下,实现自适应,
pc端保持官方样式不变,移动端实现卡片式布局浏览
最终效果
pc端:

移动端:

相关组件代码:
<template> <div class="responsive-table-wrapper"> <!-- PC端显示 --> <el-table v-if="!isMobile" :data="data" v-bind="$attrs" v-on="$listeners" ref="table" class="desktop-table"> <slot></slot> </el-table> <!-- 移动端卡片显示 --> <div v-else class="mobile-cards"> <div v-for="(row, rowIndex) in data" :key="getRowKey(row, rowIndex)" class="mobile-card" @click="handleRowClick(row, rowIndex)"> <!-- 卡片头部 - 显示主要信息 --> <div class="card-header" v-if="primaryColumn"> <div class="card-title"> <span class="title-text">{{ getCellValue(row, primaryColumn) }}</span> </div> </div> <!-- 卡片内容 --> <div class="card-content"> <div v-for="column in displayColumns" :key="column.prop || column.label" class="info-row" :class="{ 'is-important': column.important }"> <span class="label" style="color: #c0c4cc;">{{ column.label }}:</span> <div class="value"> <template v-if="column.scopedRender"> <div class="render-holder" :ref="`render-${rowIndex}-${column.prop}`"></div> </template> <!-- 普通文本显示 --> <template v-else> <span>{{ formatCellValue(row, column) }}</span> </template> </div> </div> </div> </div> <!-- 移动端无数据显示 --> <div v-if="!data || data.length === 0" class="mobile-empty"> <slot name="empty"> <div class="empty-content"> <i class="el-icon-document"></i> <p>暂无数据</p> </div> </slot> </div> </div> </div> </template> <script> export default { name: 'FlexTable', inheritAttrs: false, props: { data: { type: Array, default: () => [] }, // 移动端主显示列(通常是标题或名称) primaryField: { type: String, default: 'deviceNumber' }, // // 状态字段 // statusField: { // type: String, // default: 'status' // }, // 移动端隐藏的字段 mobileHideFields: { type: Array, default: () => [] }, // 行点击事件 rowClick: { type: Function, default: null }, // 自定义断点 mobileBreakpoint: { type: Number, default: 768 } }, data() { return { isMobile: false, columns: [] } }, computed: { primaryColumn() { if (this.primaryField) { return this.columns.find(col => col.prop === this.primaryField) } // 默认取第一个有prop的列 return this.columns.find(col => col.prop && col.type !== 'action') }, // 移动端显示的列(排除主列和隐藏列) displayColumns() { return this.columns.filter(col => { // 只排除明确隐藏的字段 if (this.mobileHideFields.includes(col.prop)) return false return true }) } }, mounted() { this.parseColumns() this.checkDevice() this.renderAllScopedSlots(); // 加上 window.addEventListener('resize', this.checkDevice) }, updated() { this.renderAllScopedSlots(); // DOM更新后重新插入 VNode }, beforeDestroy() { window.removeEventListener('resize', this.checkDevice) }, methods: { // 检测设备类型 checkDevice() { this.isMobile = window.innerWidth <= this.mobileBreakpoint }, renderAllScopedSlots() { if (!this.isMobile || !this.columns.length || !this.data.length) return; this.$nextTick(() => { this.data.forEach((row, rowIndex) => { this.columns.forEach((column) => { if (typeof column.scopedRender === 'function') { const vnode = column.scopedRender({ row, column, $index: rowIndex }); const refName = `render-${rowIndex}-${column.prop}`; const ref = this.$refs[refName]; const container = Array.isArray(ref) ? ref[0] : ref; if (!container || !(container instanceof HTMLElement)) return; container.innerHTML = ''; const vnodeRaw = column.scopedRender({ row, column, $index: rowIndex }); const vnodeArray = Array.isArray(vnodeRaw) ? vnodeRaw : [vnodeRaw]; const needWrap = vnodeArray.length > 1 || (vnodeArray.length === 1 && vnodeArray[0].tag === undefined); const renderFn = h => needWrap ? h('div', {}, vnodeArray) : vnodeArray[0]; // 不包多余的 div const vm = new this.$root.constructor({ parent: this, render: renderFn }).$mount(); container.appendChild(vm.$el); } }); }); }); }, // 解析列配置 parseColumns() { this.$nextTick(() => { if (this.$slots.default) { this.columns = this.extractColumns(this.$slots.default) } }) }, // 提取列配置 extractColumns(vnodes) { const columns = [] const extractFromVNode = (vnode) => { if (!vnode) return if (vnode.componentOptions && vnode.componentOptions.tag === 'el-table-column') { const props = vnode.componentOptions.propsData || {} const children = vnode.componentOptions.children || [] const column = { prop: props.prop || '', label: props.label, width: props.width, minWidth: props.minWidth, sortable: props.sortable, align: props.align, headerAlign: props.headerAlign, className: props.className, labelClassName: props.labelClassName, formatter: props.formatter, showOverflowTooltip: props.showOverflowTooltip, fixed: props.fixed, type: props.type, index: props.index, columnKey: props.columnKey, renderHeader: props.renderHeader, sortMethod: props.sortMethod, sortBy: props.sortBy, resizable: props.resizable, // 检查是否有作用域插槽 // scopedSlot: this.getScopedSlotName(vnode), // scopedSlot: vnode.data && vnode.data.scopedSlots && vnode.data.scopedSlots.default ? (column.prop || column.columnKey || column.label) : null, scopedRender: vnode.data && vnode.data.scopedSlots && vnode.data.scopedSlots.default ? vnode.data.scopedSlots.default : null, // 标记重要字段 important: props.important || false } // 如果没有prop但有操作,标记为操作列 if (!column.prop && children.length > 0) { column.type = 'action' // column.scopedSlot = 'action' } columns.push(column) } // 递归处理子节点 if (vnode.children) { vnode.children.forEach(extractFromVNode) } if (vnode.componentOptions && vnode.componentOptions.children) { vnode.componentOptions.children.forEach(extractFromVNode) } } vnodes.forEach(extractFromVNode) return columns }, // 获取单元格值 getCellValue(row, column) { if (!column || !column.prop) return '' const keys = column.prop.split('.') let value = row for (const key of keys) { if (value && typeof value === 'object') { value = value[key] } else { value = '' break } } return value }, // 格式化单元格值 formatCellValue(row, column) { let value = this.getCellValue(row, column) // 如果有formatter函数 if (column.formatter && typeof column.formatter === 'function') { return column.formatter(row, column, value) } // 默认格式化 if (value === null || value === undefined) { return '' } return value }, // 获取状态组件 getStatusComponent(row, column) { // 默认返回 el-tag return 'el-tag' }, // 获取状态属性 getStatusProps(row, column) { const value = this.getCellValue(row, column) // 默认状态映射 const statusMap = { 0: { type: 'success' }, 1: { type: 'danger' }, 2: { type: 'info' }, 3: { type: 'warning' }, '正常': { type: 'success' }, '异常': { type: 'danger' }, '故障': { type: 'danger' }, '关闭': { type: 'info' }, '维修': { type: 'warning' } } return { size: 'mini', ...statusMap[value] } }, // 获取状态文本 getStatusText(row, column) { const value = this.getCellValue(row, column) // 状态文本映射 const textMap = { 0: '正常', 1: '故障', 2: '关闭', 3: '维修' } return textMap[value] || value }, // 获取行key getRowKey(row, index) { if (this.$attrs['row-key']) { if (typeof this.$attrs['row-key'] === 'function') { return this.$attrs['row-key'](row) } return row[this.$attrs['row-key']] } return index }, // 行点击处理 handleRowClick(row, index) { if (this.rowClick) { this.rowClick(row, index) } this.$emit('row-click', row, index) }, // 暴露表格方法 clearSelection() { if (this.$refs.table) { this.$refs.table.clearSelection() } }, toggleRowSelection(row, selected) { if (this.$refs.table) { this.$refs.table.toggleRowSelection(row, selected) } }, toggleAllSelection() { if (this.$refs.table) { this.$refs.table.toggleAllSelection() } }, setCurrentRow(row) { if (this.$refs.table) { this.$refs.table.setCurrentRow(row) } }, clearSort() { if (this.$refs.table) { this.$refs.table.clearSort() } }, clearFilter(columnKey) { if (this.$refs.table) { this.$refs.table.clearFilter(columnKey) } }, doLayout() { if (this.$refs.table) { this.$refs.table.doLayout() } }, sort(prop, order) { if (this.$refs.table) { this.$refs.table.sort(prop, order) } } }, watch: { '$slots.default': { handler() { this.parseColumns() }, deep: true } } } </script> <style scoped> .responsive-table-wrapper { width: 100%; } /* 移动端样式 */ .mobile-cards { display: flex; flex-direction: column; gap: 12px; } .mobile-card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid #ebeef5; transition: all 0.3s ease; cursor: pointer; } .mobile-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transform: translateY(-1px); } .mobile-card:active { transform: translateY(0); } .card-header { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; } .card-title { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } .title-text { font-weight: 600; font-size: 16px; color: #303133; flex: 1; min-width: 0; word-break: break-all; } .status-tag { flex-shrink: 0; } .card-content { display: flex; flex-direction: column; gap: 8px; } .info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 6px 0; border-bottom: 1px solid #f8f9fa; min-height: 32px; } .info-row:last-child { border-bottom: none; } .info-row.is-important { background: #f0f9ff; margin: 0 -8px; padding: 8px; border-radius: 4px; border-bottom: none; } .label { font-size: 14px; color: #606266; font-weight: 500; min-width: 80px; max-width: 120px; flex-shrink: 0; line-height: 1.4; } .value { font-size: 14px; color: #303133; text-align: right; flex: 1; margin-left: 12px; word-break: break-all; line-height: 1.4; min-width: 0; } .value>>>.el-button { margin-left: 8px; padding: 4px 8px; font-size: 12px; } .value>>>.el-button:first-child { margin-left: 0; } /* 空数据样式 */ .mobile-empty { text-align: center; padding: 40px 20px; color: #909399; } .empty-content i { font-size: 48px; color: #c0c4cc; margin-bottom: 16px; } .empty-content p { margin: 0; font-size: 14px; } /* 桌面端保持原有样式 */ .desktop-table { width: 100%; } /* 响应式断点 */ @media (max-width: 480px) { .mobile-card { margin: 0 -8px; border-radius: 0; border-left: none; border-right: none; } .card-title { flex-direction: column; align-items: flex-start; } .title-text { margin-bottom: 8px; } .info-row { flex-direction: column; align-items: flex-start; gap: 4px; } .value { text-align: left; margin-left: 0; } } </style>
使用示例
<flex-table :data="dataList" style="width: 100%" v-loading="loading" primary-field="number"> <el-table-column prop="organization" label="机构" sortable align="center"></el-table-column> <el-table-column prop="number" label="仪器编号" sortable align="center"></el-table-column> <el-table-column prop="sn4g" label="4G模块编号" sortable align="center"></el-table-column> <el-table-column prop="state" label="状态" align="center"> <template slot-scope="scope"> <el-tag-mid v-if="scope.row.state==0" type="success""> 正常 </el-tag-mid> <el-tag-mid v-else-if=" scope.row.state==1" type="danger"> 故障 </el-tag-mid> <el-tag-mid v-else-if="scope.row.state==2" type="info"> 关闭 </el-tag-mid> <el-tag-mid v-else-if="scope.row.state==3" type="warning"> 维修 </el-tag-mid> </template> </el-table-column> <el-table-column prop="deliver_date" label="发货时间" align="center"> </el-table-column> <el-table-column prop="socketHeartLastTime" label="心跳最后更新时间" align="center"> </el-table-column> <el-table-column label="操作" width="240" type="action" > <template slot-scope="scope"> <el-button type="text" icon="el-icon-edit" @click="editInfo(scope.row)">编辑</el-button> <el-button type="text" icon="el-icon-delete" @click="handelDelete(scope.row.id)" style="color:#F56C6C">删除</el-button> </template> </el-table-column> </flex-table>
版权所有,转载请注明原始链接
谢绝转载至csdn
2025年7月2日更新:
修复了存在多个模板列的时候,无法正确渲染的问题
<template> <div class="responsive-table-wrapper"> <!-- PC端显示 --> <el-table v-if="!isMobile" :data="data" v-bind="$attrs" v-on="$listeners" ref="table" class="desktop-table"> <slot></slot> </el-table> <!-- 移动端卡片显示 --> <div v-else class="mobile-cards"> <div v-for="(row, rowIndex) in data" :key="getRowKey(row, rowIndex)" class="mobile-card" @click="handleRowClick(row, rowIndex)"> <!-- 卡片头部 - 显示主要信息 --> <div class="card-header" v-if="primaryColumn"> <div class="card-title"> <span class="title-text">{{ getCellValue(row, primaryColumn) }}</span> </div> </div> <!-- 卡片内容 --> <div class="card-content"> <div v-for="column in displayColumns" :key="column.prop || column.label" class="info-row" :class="{ 'is-important': column.important }"> <span class="label" style="color: #c0c4cc;">{{ column.label }}:</span> <div class="value"> <template v-if="column.scopedRender"> <div class="render-holder" :ref="`render-${rowIndex}-${column.prop}`"></div> </template> <!-- 普通文本显示 --> <template v-else> <span>{{ formatCellValue(row, column) }}</span> </template> </div> </div> </div> </div> <!-- 移动端无数据显示 --> <div v-if="!data || data.length === 0" class="mobile-empty"> <slot name="empty"> <div class="empty-content"> <i class="el-icon-document"></i> <p>暂无数据</p> </div> </slot> </div> </div> </div> </template> <script> export default { name: 'FlexTable', inheritAttrs: false, props: { data: { type: Array, default: () => [] }, // 移动端主显示列(通常是标题或名称) primaryField: { type: String, default: 'deviceNumber' }, // // 状态字段 // statusField: { // type: String, // default: 'status' // }, // 移动端隐藏的字段 mobileHideFields: { type: Array, default: () => [] }, // 行点击事件 rowClick: { type: Function, default: null }, // 自定义断点 mobileBreakpoint: { type: Number, default: 768 } }, data() { return { isMobile: false, columns: [] } }, computed: { primaryColumn() { if (this.primaryField) { return this.columns.find(col => col.prop === this.primaryField) } // 默认取第一个有prop的列 return this.columns.find(col => col.prop) }, // 移动端显示的列(排除主列和隐藏列) displayColumns() { return this.columns.filter(col => { // 只排除明确隐藏的字段 if (this.mobileHideFields.includes(col.prop)) return false return true }) } }, mounted() { this.parseColumns() this.checkDevice() this.renderAllScopedSlots(); // 加上 window.addEventListener('resize', this.checkDevice) }, updated() { this.renderAllScopedSlots(); // DOM更新后重新插入 VNode }, beforeDestroy() { window.removeEventListener('resize', this.checkDevice) }, methods: { // 检测设备类型 checkDevice() { this.isMobile = window.innerWidth <= this.mobileBreakpoint }, renderAllScopedSlots() { if (!this.isMobile || !this.columns.length || !this.data.length) return; this.$nextTick(() => { console.log('this.columns', this.columns); this.data.forEach((row, rowIndex) => { this.columns.forEach((column, columnIndex) => { if (typeof column.scopedRender === 'function') { const vnode = column.scopedRender({ row, column, $index: rowIndex }); const refName = `render-${rowIndex}-${column.prop}`; // console.log('refName',refName); // console.log('columnIndex',columnIndex); // console.log('rowIndex',columnIndex); const ref = this.$refs[refName]; const container = Array.isArray(ref) ? ref[0] : ref; if (!container || !(container instanceof HTMLElement)) return; container.innerHTML = ''; const vnodeRaw = column.scopedRender({ row, column, $index: rowIndex }); const vnodeArray = Array.isArray(vnodeRaw) ? vnodeRaw : [vnodeRaw]; const needWrap = vnodeArray.length > 1 || (vnodeArray.length === 1 && vnodeArray[0].tag === undefined); const renderFn = h => needWrap ? h('div', {}, vnodeArray) : vnodeArray[0]; // 不包多余的 div const vm = new this.$root.constructor({ parent: this, render: renderFn }).$mount(); console.log('vm.el', vm.$el); container.appendChild(vm.$el); } }); }); }); }, // 解析列配置 parseColumns() { this.$nextTick(() => { if (this.$slots.default) { this.columns = this.extractColumns(this.$slots.default) } }) }, // 提取列配置 extractColumns(vnodes) { const columns = [] let actionColumnIndex = 0; // 添加操作列索引计数器 const extractFromVNode = (vnode) => { if (!vnode) return if (vnode.componentOptions && vnode.componentOptions.tag === 'el-table-column') { const props = vnode.componentOptions.propsData || {} const children = vnode.componentOptions.children || [] const column = { prop: props.prop || '', label: props.label, width: props.width, minWidth: props.minWidth, sortable: props.sortable, align: props.align, headerAlign: props.headerAlign, className: props.className, labelClassName: props.labelClassName, formatter: props.formatter, showOverflowTooltip: props.showOverflowTooltip, fixed: props.fixed, type: props.type, index: props.index, columnKey: props.columnKey, renderHeader: props.renderHeader, sortMethod: props.sortMethod, sortBy: props.sortBy, resizable: props.resizable, // 检查是否有作用域插槽 // scopedSlot: this.getScopedSlotName(vnode), // scopedSlot: vnode.data && vnode.data.scopedSlots && vnode.data.scopedSlots.default ? (column.prop || column.columnKey || column.label) : null, scopedRender: vnode.data && vnode.data.scopedSlots && vnode.data.scopedSlots.default ? vnode.data.scopedSlots.default : null, // 标记重要字段 important: props.important || false } // 如果没有prop但有操作,标记为操作列并生成唯一标识 if (!column.prop) { if (children.length > 0) { // column.type = 'action' column.prop = `action_${actionColumnIndex++}` // 生成唯一的prop } else { column.prop = `slot_${actionColumnIndex++}` // 生成唯一的prop } } columns.push(column) } // 递归处理子节点 if (vnode.children) { vnode.children.forEach(extractFromVNode) } if (vnode.componentOptions && vnode.componentOptions.children) { vnode.componentOptions.children.forEach(extractFromVNode) } } vnodes.forEach(extractFromVNode) return columns }, // 获取单元格值 getCellValue(row, column) { if (!column || !column.prop) return '' const keys = column.prop.split('.') let value = row for (const key of keys) { if (value && typeof value === 'object') { value = value[key] } else { value = '' break } } return value }, // 格式化单元格值 formatCellValue(row, column) { let value = this.getCellValue(row, column) // 如果有formatter函数 if (column.formatter && typeof column.formatter === 'function') { return column.formatter(row, column, value) } // 默认格式化 if (value === null || value === undefined) { return '' } return value }, // 获取状态组件 getStatusComponent(row, column) { // 默认返回 el-tag return 'el-tag' }, // 获取状态属性 getStatusProps(row, column) { const value = this.getCellValue(row, column) // 默认状态映射 const statusMap = { 0: { type: 'success' }, 1: { type: 'danger' }, 2: { type: 'info' }, 3: { type: 'warning' }, '正常': { type: 'success' }, '异常': { type: 'danger' }, '故障': { type: 'danger' }, '关闭': { type: 'info' }, '维修': { type: 'warning' } } return { size: 'mini', ...statusMap[value] } }, // 获取状态文本 getStatusText(row, column) { const value = this.getCellValue(row, column) // 状态文本映射 const textMap = { 0: '正常', 1: '故障', 2: '关闭', 3: '维修' } return textMap[value] || value }, // 获取行key getRowKey(row, index) { if (this.$attrs['row-key']) { if (typeof this.$attrs['row-key'] === 'function') { return this.$attrs['row-key'](row) } return row[this.$attrs['row-key']] } return index }, // 行点击处理 handleRowClick(row, index) { if (this.rowClick) { this.rowClick(row, index) } this.$emit('row-click', row, index) }, // 暴露表格方法 clearSelection() { if (this.$refs.table) { this.$refs.table.clearSelection() } }, toggleRowSelection(row, selected) { if (this.$refs.table) { this.$refs.table.toggleRowSelection(row, selected) } }, toggleAllSelection() { if (this.$refs.table) { this.$refs.table.toggleAllSelection() } }, setCurrentRow(row) { if (this.$refs.table) { this.$refs.table.setCurrentRow(row) } }, clearSort() { if (this.$refs.table) { this.$refs.table.clearSort() } }, clearFilter(columnKey) { if (this.$refs.table) { this.$refs.table.clearFilter(columnKey) } }, doLayout() { if (this.$refs.table) { this.$refs.table.doLayout() } }, sort(prop, order) { if (this.$refs.table) { this.$refs.table.sort(prop, order) } } }, watch: { '$slots.default': { handler() { this.parseColumns() }, deep: true } } } </script> <style scoped> .responsive-table-wrapper { width: 100%; } /* 移动端样式 */ .mobile-cards { display: flex; flex-direction: column; gap: 12px; } .mobile-card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid #ebeef5; transition: all 0.3s ease; cursor: pointer; } .mobile-card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); transform: translateY(-1px); } .mobile-card:active { transform: translateY(0); } .card-header { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; } .card-title { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } .title-text { font-weight: 600; font-size: 16px; color: #303133; flex: 1; min-width: 0; word-break: break-all; } .status-tag { flex-shrink: 0; } .card-content { display: flex; flex-direction: column; gap: 8px; } .info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 6px 0; border-bottom: 1px solid #f8f9fa; min-height: 32px; } .info-row:last-child { border-bottom: none; } .info-row.is-important { background: #f0f9ff; margin: 0 -8px; padding: 8px; border-radius: 4px; border-bottom: none; } .label { font-size: 14px; color: #606266; font-weight: 500; min-width: 80px; max-width: 120px; flex-shrink: 0; line-height: 1.4; } .value { font-size: 14px; color: #303133; text-align: right; flex: 1; margin-left: 12px; word-break: break-all; line-height: 1.4; min-width: 0; } .value>>>.el-button { margin-left: 8px; padding: 4px 8px; font-size: 12px; } .value>>>.el-button:first-child { margin-left: 0; } /* 空数据样式 */ .mobile-empty { text-align: center; padding: 40px 20px; color: #909399; } .empty-content i { font-size: 48px; color: #c0c4cc; margin-bottom: 16px; } .empty-content p { margin: 0; font-size: 14px; } /* 桌面端保持原有样式 */ .desktop-table { width: 100%; } /* 响应式断点 */ @media (max-width: 480px) { .mobile-card { margin: 0 -8px; border-radius: 0; border-left: none; border-right: none; } .card-title { flex-direction: column; align-items: flex-start; } .title-text { margin-bottom: 8px; } .info-row { flex-direction: column; align-items: flex-start; gap: 4px; } .value { text-align: left; margin-left: 0; } } </style>
浙公网安备 33010602011771号