feat: GrTable 移动端表格组件 + manager/Projects 试点
业务背景 - 项目里 17+ 列的列表页 (manager/Projects, Meetings, AdminUsers 等) 在手机端只能 横向滚动, 操作体验差. - refer/gr-table.vue 是 Vue2 + Vant 组件, Vue3 不能直接用 (componentOptions 私有 API 已被 Vue3 移除). - 新建 Vue3 + EP 兼容版本 GrTable, 吸收"主列显示 / 次要列折叠到展开行"思想. 组件设计 - props: data, mainCols (默认 = 第一个 prop + 最后一个 prop), mobileBreakpoint - 通过 slots.default() 取 vnode, 读 vnode.props.prop/label (Vue3 思路, 替代 Vue2 componentOptions 私有 API) - v-bind="\$attrs" 全透传 el-table 原生 props/events - el-table 引用方法 (clearSelection / toggleRowSelection 等) defineExpose 暴露 manager/Projects 试点 - 引入 GrTable, :main-cols="['projectNo', 'projectName']" - 手机端: 只显示项目编号 + 项目名称 + 操作列, 其他 13 列折叠到展开行 - 桌面端: 与原 el-table 完全一致 (所有 17 列) - 操作列 (label=操作, 无 prop) 自动保留 (因为 shouldRenderColumn 跳过无 prop 列)
This commit is contained in:
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="gr-table-container">
|
||||||
|
<div class="gr-table-filter">
|
||||||
|
<div class="gr-query-wrap">
|
||||||
|
<div>筛选</div>
|
||||||
|
<van-field class="gr-query" v-model="queryVal"></van-field>
|
||||||
|
<van-icon name="search" size="18"></van-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table class="gr-table" ref="mesSysTable" :data="data2Show" :border="border" :height="height"
|
||||||
|
:max-height="maxHeight" :stripe="stripe" :size="size" :fit="fit" :show-header="showHeader"
|
||||||
|
:highlight-current-row="highlightCurrentRow" :highlight-selection-row="highlightSelectionRow"
|
||||||
|
:current-row-key="currentRowKey" :row-class-name="rowClassName" :row-style="rowStyle" :cell-class-name="cellClassName"
|
||||||
|
:cell-style="cellStyle" :header-row-class-name="headerRowClassName" :header-row-style="headerRowStyle"
|
||||||
|
:header-cell-class-name="headerCellClassName" :header-cell-style="headerCellStyle" :row-key="rowKey"
|
||||||
|
:empty-text="emptyText" :default-expand-all="defaultExpandAll" :expand-row-keys="expandRowKeys"
|
||||||
|
:default-sort="defaultSort" :tooltip-effect="tooltipEffect" :show-summary="showSummary" :summary-method="summaryMethod"
|
||||||
|
:sum-text="sumText" :span-method="spanMethod" :select-on-indeterminate="selectOnIndeterminate" :indent="indent"
|
||||||
|
:lazy="lazy" :load="load" :tree-props="treeProps"
|
||||||
|
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||||
|
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||||
|
@cell-mouse-leave="(evt) => $emit('cell-mouse-leave',evt)" @cell-click="(evt) => $emit('cell-click',evt)"
|
||||||
|
@cell-dblclick="(evt) => $emit('cell-dblclick',evt)" @row-click="(evt) => $emit('row-click',evt)"
|
||||||
|
@row-contextmenu="(evt) => $emit('row-contextmenu',evt)" @row-dblclick="(evt) => $emit('row-dblclick',evt)"
|
||||||
|
@header-click="(evt) => $emit('header-click',evt)" @header-contextmenu="(evt) => $emit('header-contextmenu',evt)"
|
||||||
|
@sort-change="(evt) => $emit('sort-change',evt)" @filter-change="(evt) => $emit('filter-change',evt)"
|
||||||
|
@current-change="(evt) => $emit('current-change',evt)" @header-dragend="(evt) => $emit('header-dragend',evt)"
|
||||||
|
@expand-change="(evt) => $emit('expand-change',evt)" >
|
||||||
|
<el-table-column width="30" type="expand" v-if="meta">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||||
|
<el-descriptions-item v-for="(item, index) in meta" :label="item.label" :key="index">{{scope.row[item.prop]}}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot>
|
||||||
|
</slot>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { Field } from 'vant';
|
||||||
|
Vue.use(Field);
|
||||||
|
import {Icon} from "vant";
|
||||||
|
Vue.use(Icon);
|
||||||
|
export default {
|
||||||
|
props: ["data","meta" ,"major","border","height","maxHeight","stripe","size","fit","showHeader",
|
||||||
|
"highlightCurrentRow","highlightSelectionRow","currentRowKey","rowClassName","rowStyle","cellClassName",
|
||||||
|
"cellStyle","headerRowClassName", "headerRowStyle","headerCellClassName","headerCellStyle",
|
||||||
|
"rowKey", "emptyText","defaultExpandAll", "expandRowKeys","defaultSort","tooltipEffect","sumText","showSummary",
|
||||||
|
"summaryMethod", "spanMethod","selectOnIndeterminate","indent","lazy","load","treeProps",
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data2Show: [],
|
||||||
|
slight: null,
|
||||||
|
mainCol: this.major,
|
||||||
|
rows: null,
|
||||||
|
queryVal: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data() {
|
||||||
|
this.refreshData();
|
||||||
|
},
|
||||||
|
queryVal(val) {
|
||||||
|
// console.log("queryval ", val);
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDataList(row) {
|
||||||
|
return Object.keys(row)
|
||||||
|
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||||
|
return {
|
||||||
|
label: this.slight.find((s) => s.prop == key).label,
|
||||||
|
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearSelection(...args) { this.$refs['mesSysTable'].clearSelection(...args)},
|
||||||
|
toggleRowSelection (...args) {this.$refs['mesSysTable'].toggleRowSelection(...args)},
|
||||||
|
toggleAllSelection (...args) { this.$refs['mesSysTable'].toggleAllSelection(...args)},
|
||||||
|
toggleRowExpansion (...args) { this.$refs['mesSysTable'].toggleRowExpansion(...args)},
|
||||||
|
setCurrentRow (...args) { this.$refs['mesSysTable'].setCurrentRow(...args)},
|
||||||
|
clearSort (...args) { this.$refs['mesSysTable'].clearSort(...args)},
|
||||||
|
clearFilter (...args){ this.$refs['mesSysTable'].clearFilter(...args)},
|
||||||
|
doLayout (...args){ this.$refs['mesSysTable'].doLayout(...args)},
|
||||||
|
sort (...args){ this.$refs['mesSysTable'].sort(...args)},
|
||||||
|
refreshData() {
|
||||||
|
if (this.queryVal) {
|
||||||
|
// this.$showLoading();
|
||||||
|
// console.log("queryval is",this.queryVal);
|
||||||
|
this.data2Show = this.data.filter(item => {
|
||||||
|
return JSON.stringify(item).indexOf(this.queryVal) != -1;
|
||||||
|
})
|
||||||
|
// this.$hideLoading();
|
||||||
|
} else {
|
||||||
|
this.data2Show = this.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// created() {
|
||||||
|
// this.data2Show = JSON.parse(JSON.stringify(this.data));
|
||||||
|
// },
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/assets/styles/vant.scss";
|
||||||
|
|
||||||
|
::v-deep .van-cell {
|
||||||
|
padding: 0px !important
|
||||||
|
}
|
||||||
|
::v-deep .el-table .cell {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.gr-table-container {
|
||||||
|
padding: 0 8px;
|
||||||
|
.gr-table-filter {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid $gr-border;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
.gr-query-wrap {
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
.gr-query {
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep th.el-table__cell {
|
||||||
|
background: #0ECD4916 !important;
|
||||||
|
font-family: PingFangSC-Medium;
|
||||||
|
color: #323233;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table__cell {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
::v-deep .el-descriptions__body {
|
||||||
|
font-size: 12px !important;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-table v-if="showTable" ref="mesSysTable" :data="data" :border="border" :height="height"
|
||||||
|
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||||
|
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||||
|
>
|
||||||
|
<el-table-column type="expand" v-if="slight && slight[0]">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||||
|
<el-descriptions-item v-for="(item, index) in getDataList(scope.row)" :label="item.label" :key="index">{{item.value}}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot name="default">
|
||||||
|
</slot>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["data", "major","border","height"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mainCol: this.major,
|
||||||
|
rows: null,
|
||||||
|
showTable: true,
|
||||||
|
refreshing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDataList(row) {
|
||||||
|
return Object.keys(row)
|
||||||
|
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||||
|
return {
|
||||||
|
label: this.slight.find((s) => s.prop == key).label,
|
||||||
|
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initData() {
|
||||||
|
let length = this.$slots.default.length, rows = this.$slots.default;
|
||||||
|
// if (window.innerWidth < 768) { //判断PDA
|
||||||
|
if (!this.major || !this.major[0]) { //如果没有指定重要的列,默认第一列(一般是名称)和最后一列(一般是操作)
|
||||||
|
this.mainCol = [rows[0].componentOptions.propsData.prop,rows[length-1].componentOptions.propsData.prop]
|
||||||
|
}
|
||||||
|
this.slight = rows.filter((item) => { //筛选不重要的列,展示到expand中
|
||||||
|
return !(this.mainCol.some((main) => {
|
||||||
|
return main == item.componentOptions.propsData.prop;
|
||||||
|
}));
|
||||||
|
}).map((item) => {
|
||||||
|
return item.componentOptions.propsData;
|
||||||
|
});
|
||||||
|
this.rows = rows.filter((item) => { //去掉不重要的列
|
||||||
|
return this.mainCol.some((main) => {
|
||||||
|
return main == item.componentOptions.propsData.prop;
|
||||||
|
}) || !item.componentOptions.propsData.prop;
|
||||||
|
});
|
||||||
|
this.$slots.default = this.rows;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.initData();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
if (!this.refreshing) {
|
||||||
|
this.refreshing = true;
|
||||||
|
this.showTable = false;
|
||||||
|
} else{
|
||||||
|
setTimeout(()=> {
|
||||||
|
this.showTable = true;
|
||||||
|
this.initData();
|
||||||
|
},0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
::v-deep th.el-table__cell {
|
||||||
|
background: #0ECD4916 !important;
|
||||||
|
font-family: PingFangSC-Medium;
|
||||||
|
// font-size: 16px;
|
||||||
|
color: #323233;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table__cell {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-table ref="mesSysTable" :data="data" :border="border" :height="height"
|
||||||
|
:max-height="maxHeight" :stripe="stripe" :size="size" :fit="fit" :show-header="showHeader"
|
||||||
|
:highlight-current-row="highlightCurrentRow" :highlight-selection-row="highlightSelectionRow"
|
||||||
|
:current-row-key="currentRowKey" :row-class-name="rowClassName" :row-style="rowStyle" :cell-class-name="cellClassName"
|
||||||
|
:cell-style="cellStyle" :header-row-class-name="headerRowClassName" :header-row-style="headerRowStyle"
|
||||||
|
:header-cell-class-name="headerCellClassName" :header-cell-style="headerCellStyle" :row-key="rowKey"
|
||||||
|
:empty-text="emptyText" :default-expand-all="defaultExpandAll" :expand-row-keys="expandRowKeys"
|
||||||
|
:default-sort="defaultSort" :tooltip-effect="tooltipEffect" :show-summary="showSummary" :summary-method="summaryMethod"
|
||||||
|
:sum-text="sumText" :span-method="spanMethod" :select-on-indeterminate="selectOnIndeterminate" :indent="indent"
|
||||||
|
:lazy="lazy" :load="load" :tree-props="treeProps"
|
||||||
|
@select="(evt) => $emit('select',evt)" @select-all="(evt) => $emit('select-all',evt)"
|
||||||
|
@selection-change="(evt) => $emit('selection-change',evt)" @cell-mouse-enter="(evt) => $emit('cell-mouse-enter',evt)"
|
||||||
|
@cell-mouse-leave="(evt) => $emit('cell-mouse-leave',evt)" @cell-click="(evt) => $emit('cell-click',evt)"
|
||||||
|
@cell-dblclick="(evt) => $emit('cell-dblclick',evt)" @row-click="(evt) => $emit('row-click',evt)"
|
||||||
|
@row-contextmenu="(evt) => $emit('row-contextmenu',evt)" @row-dblclick="(evt) => $emit('row-dblclick',evt)"
|
||||||
|
@header-click="(evt) => $emit('header-click',evt)" @header-contextmenu="(evt) => $emit('header-contextmenu',evt)"
|
||||||
|
@sort-change="(evt) => $emit('sort-change',evt)" @filter-change="(evt) => $emit('filter-change',evt)"
|
||||||
|
@current-change="(evt) => $emit('current-change',evt)" @header-dragend="(evt) => $emit('header-dragend',evt)"
|
||||||
|
@expand-change="(evt) => $emit('expand-change',evt)" >
|
||||||
|
<el-table-column type="expand" v-if="slight && slight[0]">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-descriptions :column="1" style="margin: 0 20px;">
|
||||||
|
<el-descriptions-item v-for="(item, index) in getDataList(scope.row)" :label="item.label" :key="index">{{item.value}}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<slot>
|
||||||
|
</slot>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["data", "major","border","height","maxHeight","stripe","size","fit","showHeader",
|
||||||
|
"highlightCurrentRow","highlightSelectionRow","currentRowKey","rowClassName","rowStyle","cellClassName",
|
||||||
|
"cellStyle","headerRowClassName", "headerRowStyle","headerCellClassName","headerCellStyle",
|
||||||
|
"rowKey", "emptyText","defaultExpandAll", "expandRowKeys","defaultSort","tooltipEffect","sumText","showSummary",
|
||||||
|
"summaryMethod", "spanMethod","selectOnIndeterminate","indent","lazy","load","treeProps",
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
slight: null,
|
||||||
|
mainCol: this.major,
|
||||||
|
rows: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getDataList(row) {
|
||||||
|
return Object.keys(row)
|
||||||
|
.filter((key) => this.slight.some((s) => s.prop == key)).map((key) => {
|
||||||
|
return {
|
||||||
|
label: this.slight.find((s) => s.prop == key).label,
|
||||||
|
value: row[this.slight.find((s) => s.prop == key).prop],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearSelection(...args) { this.$refs['mesSysTable'].clearSelection(...args)},
|
||||||
|
toggleRowSelection (...args) {this.$refs['mesSysTable'].toggleRowSelection(...args)},
|
||||||
|
toggleAllSelection (...args) { this.$refs['mesSysTable'].toggleAllSelection(...args)},
|
||||||
|
toggleRowExpansion (...args) { this.$refs['mesSysTable'].toggleRowExpansion(...args)},
|
||||||
|
setCurrentRow (...args) { this.$refs['mesSysTable'].setCurrentRow(...args)},
|
||||||
|
clearSort (...args) { this.$refs['mesSysTable'].clearSort(...args)},
|
||||||
|
clearFilter (...args){ this.$refs['mesSysTable'].clearFilter(...args)},
|
||||||
|
doLayout (...args){ this.$refs['mesSysTable'].doLayout(...args)},
|
||||||
|
sort (...args){ this.$refs['mesSysTable'].sort(...args)},
|
||||||
|
|
||||||
|
initData() {
|
||||||
|
let length = this.$slots.default.length, rows = this.$slots.default;
|
||||||
|
// if (window.innerWidth < 768) { //判断PDA
|
||||||
|
if (!this.major || !this.major[0]) { //如果没有指定重要的列,默认第一列(一般是名称)和最后一列(一般是操作)
|
||||||
|
this.mainCol = [rows[0].componentOptions.propsData.prop,rows[length-1].componentOptions.propsData.prop]
|
||||||
|
}
|
||||||
|
this.slight = rows.filter((item) => { //筛选不重要的列,展示到expand中
|
||||||
|
return !(this.mainCol.some((main) => {
|
||||||
|
return main == item.componentOptions.propsData.prop;
|
||||||
|
}));
|
||||||
|
}).map((item) => {
|
||||||
|
return item.componentOptions.propsData;
|
||||||
|
});
|
||||||
|
this.rows = rows.filter((item) => { //去掉不重要的列
|
||||||
|
return this.mainCol.some((main) => {
|
||||||
|
return main == item.componentOptions.propsData.prop;
|
||||||
|
}) || !item.componentOptions.propsData.prop;
|
||||||
|
});
|
||||||
|
this.$slots.default = this.rows;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.initData();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
console.log("updated!!!!");
|
||||||
|
// this.initData();
|
||||||
|
// this.$slots.default = this.rows;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
::v-deep th.el-table__cell {
|
||||||
|
background: #0ECD4916 !important;
|
||||||
|
font-family: PingFangSC-Medium;
|
||||||
|
// font-size: 16px;
|
||||||
|
color: #323233;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table__cell {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<!--
|
||||||
|
GrTable — el-table 移动端友好封装 (Vue 3 + Element Plus)
|
||||||
|
-----------------------------------------------------------------
|
||||||
|
背景: 通用 el-table 在窄屏 (≤768px) 会横向溢出, 用户需滑动才能看到所有列.
|
||||||
|
本组件吸收 refer/gr-table.vue 的"主列显示 / 次要列折叠到展开行"思想, 但用
|
||||||
|
Vue 3 + EP 思路重写:
|
||||||
|
- 通过 slots.default() 拿到所有 vnode (Vue2 componentOptions 私有 API 已
|
||||||
|
废弃), 取 vnode.props.prop / props.label
|
||||||
|
- mainCols: 移动端显示的主列名 (prop), 未指定时 = 第一个有 prop 的列 + 最
|
||||||
|
后一个 prop 列 (一般是项目编号 + 操作)
|
||||||
|
- 操作列识别: 没有 prop 但有自定义 slot 内容 (label="操作") → 始终保留
|
||||||
|
- 非主列在移动端移到 type="expand" 的 el-table-column 中, 通过 el-descriptions
|
||||||
|
展示 (1 列垂直堆叠)
|
||||||
|
所有 el-table 原生 prop/event 通过 v-bind="$attrs" 全透传, 调用方用法几乎不变
|
||||||
|
(只在 el-table 外包一层 + 加 mainCols 可选属性).
|
||||||
|
|
||||||
|
Vue 版本: Vue 3 + Vite + Element Plus (script setup, Composition API).
|
||||||
|
弃用 refer/gr-table.vue 的 Vue 2 Options API + componentOptions 私有 API.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:data="data"
|
||||||
|
>
|
||||||
|
<!-- 移动端展开行: 只在 ≤breakpoint 渲染, 显示非主列 -->
|
||||||
|
<el-table-column v-if="isMobile && collapsedCols.length" type="expand">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-descriptions :column="1" size="small" class="gr-mobile-desc">
|
||||||
|
<el-descriptions-item
|
||||||
|
v-for="col in collapsedCols"
|
||||||
|
:key="col.prop"
|
||||||
|
:label="col.label"
|
||||||
|
>
|
||||||
|
<span v-if="col.formatter">{{ col.formatter(row, col.prop, row[col.prop]) }}</span>
|
||||||
|
<span v-else>{{ row[col.prop] ?? '-' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
桌面: 透传所有列 (含非主列)
|
||||||
|
移动端: 只透传主列 + 操作列 (type="selection" 也保留, 它没 prop)
|
||||||
|
-->
|
||||||
|
<template v-for="(child, idx) in slotColumns" :key="idx">
|
||||||
|
<component
|
||||||
|
:is="child"
|
||||||
|
v-if="shouldRenderColumn(child, idx)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
|
||||||
|
import { ElTable, ElTableColumn, ElDescriptions, ElDescriptionsItem } from 'element-plus'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Array, default: () => [] },
|
||||||
|
/** 移动端保留的主列 prop 列表; 未指定时取第一个有 prop 的列 + 最后一个 */
|
||||||
|
mainCols: { type: Array, default: null },
|
||||||
|
/** 移动端断点 (≤此宽度启用折叠模式) */
|
||||||
|
mobileBreakpoint: { type: Number, default: 768 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRef = ref(null)
|
||||||
|
const isMobile = ref(false)
|
||||||
|
let mq = null
|
||||||
|
|
||||||
|
function updateMq(e) {
|
||||||
|
isMobile.value = e.matches
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
mq = window.matchMedia(`(max-width: ${props.mobileBreakpoint}px)`)
|
||||||
|
isMobile.value = mq.matches
|
||||||
|
if (mq.addEventListener) mq.addEventListener('change', updateMq)
|
||||||
|
else mq.addListener(updateMq)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (!mq) return
|
||||||
|
if (mq.removeEventListener) mq.removeEventListener('change', updateMq)
|
||||||
|
else mq.removeListener(updateMq)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拿到调用方传入的 el-table-column 子节点 (Vue 3: slots.default 是函数)
|
||||||
|
const slots = useSlots()
|
||||||
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const slotColumns = computed(() => {
|
||||||
|
const nodes = slots.default ? slots.default() : []
|
||||||
|
// 拍平: 处理 fragment 包裹 (用户在 template 里直接写多列会触发)
|
||||||
|
const flat = []
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (Array.isArray(n.children)) {
|
||||||
|
for (const c of n.children) if (c && typeof c === 'object') flat.push(c)
|
||||||
|
} else if (n && typeof n === 'object' && n.type) {
|
||||||
|
flat.push(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flat
|
||||||
|
})
|
||||||
|
|
||||||
|
// 抽取所有有 prop 的列 (排除 type=expand / type=selection / type=index, 它们用
|
||||||
|
// 自身 type 标识, 不通过 prop)
|
||||||
|
const dataCols = computed(() => {
|
||||||
|
return slotColumns.value
|
||||||
|
.map((n, idx) => ({
|
||||||
|
node: n,
|
||||||
|
originalIndex: idx,
|
||||||
|
type: n.props?.type,
|
||||||
|
prop: n.props?.prop,
|
||||||
|
label: n.props?.label || (n.props?.type === 'selection' ? '#' : (n.props?.type === 'index' ? '#' : '')),
|
||||||
|
formatter: n.props?.formatter
|
||||||
|
}))
|
||||||
|
.filter((c) => c.prop) // 只要有 prop 的列
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算主列列表 (Vue3 不用 componentOptions, 直接读 vnode.props)
|
||||||
|
const mainColsResolved = computed(() => {
|
||||||
|
if (props.mainCols && props.mainCols.length) return props.mainCols
|
||||||
|
const cols = dataCols.value
|
||||||
|
if (!cols.length) return []
|
||||||
|
// 默认: 第一个 + 最后一个 (一般是项目编号 + 操作前的"项目结束时间"列)
|
||||||
|
// 用户的 "操作" 列通常没 prop, 所以"最后一个有 prop 的列"会自然避开
|
||||||
|
return [cols[0].prop, cols[cols.length - 1].prop]
|
||||||
|
})
|
||||||
|
|
||||||
|
const collapsedCols = computed(() => {
|
||||||
|
return dataCols.value.filter((c) => !mainColsResolved.value.includes(c.prop))
|
||||||
|
})
|
||||||
|
|
||||||
|
function shouldRenderColumn(node, idx) {
|
||||||
|
if (!isMobile.value) return true // 桌面全显示
|
||||||
|
const meta = dataCols.value.find((c) => c.originalIndex === idx)
|
||||||
|
if (!meta) return true // 非数据列 (expand/selection/index/自定义 slot 列), 保留
|
||||||
|
if (!meta.prop) return true // 无 prop, 视为操作列或特殊列, 保留
|
||||||
|
return mainColsResolved.value.includes(meta.prop) // 只渲染主列
|
||||||
|
}
|
||||||
|
|
||||||
|
// 透传 el-table 引用方法 (clearSelection / toggleRowSelection 等)
|
||||||
|
defineExpose({
|
||||||
|
clearSelection: (...args) => tableRef.value?.clearSelection?.(...args),
|
||||||
|
toggleRowSelection: (...args) => tableRef.value?.toggleRowSelection?.(...args),
|
||||||
|
toggleAllSelection: (...args) => tableRef.value?.toggleAllSelection?.(...args),
|
||||||
|
toggleRowExpansion: (...args) => tableRef.value?.toggleRowExpansion?.(...args),
|
||||||
|
setCurrentRow: (...args) => tableRef.value?.setCurrentRow?.(...args),
|
||||||
|
clearSort: (...args) => tableRef.value?.clearSort?.(...args),
|
||||||
|
clearFilter: (...args) => tableRef.value?.clearFilter?.(...args),
|
||||||
|
doLayout: (...args) => tableRef.value?.doLayout?.(...args),
|
||||||
|
sort: (...args) => tableRef.value?.sort?.(...args),
|
||||||
|
/** 暴露内部 ref 供高级用法 */
|
||||||
|
tableRef
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gr-mobile-desc {
|
||||||
|
margin: 0 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.gr-mobile-desc :deep(.el-descriptions__label) {
|
||||||
|
width: 90px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
background: var(--brand-slate-50);
|
||||||
|
}
|
||||||
|
.gr-mobile-desc :deep(.el-descriptions__content) {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -81,7 +81,14 @@
|
|||||||
批量删除<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
批量删除<span v-if="selection.length" class="badge">{{ selection.length }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
|
<GrTable
|
||||||
|
:data="rows"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
:main-cols="['projectNo', 'projectName']"
|
||||||
|
@selection-change="onSel"
|
||||||
|
>
|
||||||
<el-table-column type="selection" width="44" />
|
<el-table-column type="selection" width="44" />
|
||||||
<el-table-column prop="projectNo" label="项目编号" width="140" fixed="left" />
|
<el-table-column prop="projectNo" label="项目编号" width="140" fixed="left" />
|
||||||
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
|
||||||
@@ -140,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</GrTable>
|
||||||
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,6 +252,7 @@ import request from '@/utils/request'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ArrowDown } from '@element-plus/icons-vue'
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import Preview from '@/components/Preview.vue'
|
import Preview from '@/components/Preview.vue'
|
||||||
|
import GrTable from '@/components/GrTable.vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
Reference in New Issue
Block a user