新增统一的接口调用方法

This commit is contained in:
huangchenhao 2025-10-30 16:34:29 +08:00
parent 2f10f4dfc0
commit 3721a658f8
8 changed files with 549 additions and 46 deletions

View File

@ -25,6 +25,7 @@
"pnpm": ">=8.0.0"
},
"dependencies": {
"@vueuse/core": "^14.0.0",
"dayjs": "^1.11.18",
"element-plus": "^2.11.5",
"vant": "^4.9.21"

View File

@ -0,0 +1,78 @@
<template>
<div v-if="buttons.length" class="dynamic-table-toolbar" :style="toolbarStyle">
<el-space :size="8">
<el-button
v-for="(btn, idx) in buttons"
:key="btn.key || idx"
:type="btn.type"
:icon="btn.icon"
:loading="btn.loading"
:disabled="btn.disabled || (btn.needsSelection && !selectedKeys.length)"
@click="handleClick(btn)"
>
{{ btn.text }}
</el-button>
</el-space>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
/**
* 按钮列表
*/
buttons: {
type: Array,
default: () => []
},
/**
* 已选中的行 key 数组
*/
selectedKeys: {
type: Array,
default: () => []
},
/**
* 数据源传递给按钮 onClick
*/
dataSource: {
type: Array,
default: () => []
},
/**
* 工具栏对齐方式
*/
align: {
type: String,
default: 'left',
validator: (v) => ['left', 'center', 'right'].includes(v)
}
})
const toolbarStyle = computed(() => ({
justifyContent: props.align === 'left' ? 'flex-start'
: props.align === 'right' ? 'flex-end'
: 'center'
}))
const handleClick = (button) => {
//
if (button.needsSelection && !props.selectedKeys.length) {
ElMessage.warning(button.selectionMessage || '请选择要操作的数据')
return
}
// onClick
button.onClick?.(props.selectedKeys, props.dataSource)
}
</script>
<style scoped>
.dynamic-table-toolbar {
display: flex;
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,6 @@
/**
* DynamicTable 组件导出
*/
import DynamicTable from './index.vue'
export default DynamicTable

View File

@ -0,0 +1,217 @@
<template>
<div
ref="containerRef"
class="dynamic-table-container"
:class="{ 'auto-height-enabled': autoHeight }"
>
<!-- 工具栏 -->
<TableToolbar
v-if="toolbar"
:buttons="toolbar.buttons"
:selected-keys="selectedRowKeys"
:data-source="dataSource"
:align="toolbar.align"
/>
<!-- 表格 - 直接透传所有原生属性 -->
<el-table
ref="tableRef"
:data="dataSource"
:height="computedHeight"
v-bind="$attrs"
@selection-change="handleSelectionChange"
>
<!-- 渲染列 -->
<el-table-column
v-for="(col, idx) in processedColumns"
:key="col.prop || col.label || idx"
v-bind="getColumnProps(col)"
>
<!-- 自定义渲染render slot -->
<template v-if="col.render || col.slot" #default="scope">
<component v-if="col.render" :is="col.render(scope.row, scope.column, scope.$index)" />
<slot v-else-if="col.slot" :name="col.slot" v-bind="scope" />
</template>
</el-table-column>
<!-- 透传所有插槽 -->
<template v-for="(_, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps || {}" />
</template>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="pagination"
:current-page="pagination.current"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="pagination.pageSizes || [10, 20, 50, 100]"
:layout="pagination.layout || 'total, sizes, prev, pager, next, jumper'"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import TableToolbar from './TableToolbar.vue'
import { useAutoHeight } from './useAutoHeight'
defineOptions({
name: 'DynamicTable',
inheritAttrs: false //
})
const props = defineProps({
/**
* 数据源
*/
dataSource: {
type: Array,
default: () => []
},
/**
* 列配置
*/
columns: {
type: Array,
default: () => []
},
/**
* 工具栏配置
*/
toolbar: {
type: Object,
default: null
},
/**
* 是否启用自适应高度
*/
autoHeight: {
type: Boolean,
default: false
},
/**
* 分页配置false 表示不分页
*/
pagination: {
type: [Object, Boolean],
default: false
}
})
const emit = defineEmits(['selection-change'])
const containerRef = ref(null)
const tableRef = ref(null)
const selectedRowKeys = ref([])
// ==================== ====================
const { tableHeight } = useAutoHeight(containerRef, {
enabled: props.autoHeight,
minHeight: 200
})
const computedHeight = computed(() => {
return props.autoHeight ? tableHeight.value : undefined
})
// ==================== ====================
/**
* 处理列配置,设置合理的默认值
* 原则: 仅设置默认值,不改变结构,用户配置优先
*/
const processedColumns = computed(() => {
return props.columns.map(col => {
// (selection/index/expand),
if (col.type) return col
//
return {
showOverflowTooltip: true, //
align: 'center', //
minWidth: col.width ? undefined : 100, //
...col //
}
})
})
/**
* 获取列的 props用于 v-bind
* 过滤掉自定义属性 (render, slot)
*/
const getColumnProps = (col) => {
const { render, slot, ...restProps } = col
return restProps
}
// ==================== ====================
/**
* 处理行选择改变
*/
const handleSelectionChange = (selection) => {
// key
const rowKey = props.$attrs?.rowKey || props.$attrs?.['row-key'] || 'id'
selectedRowKeys.value = selection.map(row =>
typeof rowKey === 'function' ? rowKey(row) : row[rowKey]
)
emit('selection-change', selection)
}
/**
* 处理分页页码改变
*/
const handlePageChange = (page) => {
props.pagination?.onChange?.(page, props.pagination.pageSize)
}
/**
* 处理每页条数改变
*/
const handleSizeChange = (size) => {
// ,
props.pagination?.onChange?.(1, size)
}
// ==================== ====================
defineExpose({
/**
* el-table 实例引用
*/
tableRef,
/**
* 已选中的行 key 数组
*/
selectedRowKeys,
/**
* 手动重新计算表格高度
*/
recalculate: () => {
if (props.autoHeight) {
setTimeout(() => {
tableHeight.recalculate?.()
}, 100)
}
}
})
</script>
<style scoped>
.dynamic-table-container {
display: flex;
flex-direction: column;
height: 100%;
}
.auto-height-enabled {
overflow: hidden;
}
.el-pagination {
margin-top: 16px;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,129 @@
/**
* 自适应高度 Hook
* 根据父容器和兄弟元素动态计算表格高度
*
* @param {Ref} containerRef - 容器 DOM 引用
* @param {Object} options - 配置选项
* @param {boolean} options.enabled - 是否启用自适应高度
* @param {number} options.minHeight - 最小高度
* @param {number} options.offset - 额外的高度偏移
* @param {number} options.debounce - 防抖延迟
* @returns {{tableHeight: Ref<number>, recalculate: Function}}
*/
import { ref, onMounted, onUnmounted } from 'vue'
import { useResizeObserver } from '@vueuse/core'
export function useAutoHeight(containerRef, options = {}) {
const {
enabled = true,
minHeight = 200,
offset = 0,
debounce = 100
} = options
const tableHeight = ref(400)
let debounceTimer = null
/**
* 计算表格可用高度
* 逻辑: 父容器高度 - 兄弟元素高度 - 工具栏 - 表头 - 分页器
*/
const calculateHeight = () => {
if (!enabled || !containerRef.value) return
const container = containerRef.value
const parentElement = container.parentElement
if (!parentElement) return
// 1. 获取父容器可用高度
const parentRect = parentElement.getBoundingClientRect()
const parentStyle = window.getComputedStyle(parentElement)
const parentPaddingTop = parseFloat(parentStyle.paddingTop) || 0
const parentPaddingBottom = parseFloat(parentStyle.paddingBottom) || 0
// 2. 计算兄弟元素占用的高度
let siblingsHeight = 0
Array.from(parentElement.children).forEach(child => {
if (child !== container) {
const childRect = child.getBoundingClientRect()
const childStyle = window.getComputedStyle(child)
const marginTop = parseFloat(childStyle.marginTop) || 0
const marginBottom = parseFloat(childStyle.marginBottom) || 0
siblingsHeight += childRect.height + marginTop + marginBottom
}
})
let availableHeight = parentRect.height - parentPaddingTop - parentPaddingBottom - siblingsHeight
// 3. 减去容器内部组件高度
// 工具栏
const toolbar = container.querySelector('.dynamic-table-toolbar')
if (toolbar) {
const toolbarRect = toolbar.getBoundingClientRect()
const toolbarStyle = window.getComputedStyle(toolbar)
const marginBottom = parseFloat(toolbarStyle.marginBottom) || 0
availableHeight -= (toolbarRect.height + marginBottom)
}
// 表头
const thead = container.querySelector('.el-table__header-wrapper')
if (thead) {
availableHeight -= thead.getBoundingClientRect().height
}
// 分页器
const pagination = container.querySelector('.el-pagination')
if (pagination) {
const paginationRect = pagination.getBoundingClientRect()
const paginationStyle = window.getComputedStyle(pagination)
const marginTop = parseFloat(paginationStyle.marginTop) || 0
const marginBottom = parseFloat(paginationStyle.marginBottom) || 0
availableHeight -= (paginationRect.height + marginTop + marginBottom)
}
// 4. 应用偏移和最小高度限制
const finalHeight = Math.max(availableHeight - offset, minHeight)
tableHeight.value = finalHeight
}
/**
* 防抖包装
*/
const debouncedCalculate = () => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(calculateHeight, debounce)
}
// 监听容器尺寸变化
useResizeObserver(containerRef, debouncedCalculate)
// 组件挂载后初始化
onMounted(() => {
if (!enabled || !containerRef.value) return
// 监听父容器尺寸变化
const parentElement = containerRef.value.parentElement
if (parentElement) {
useResizeObserver(parentElement, debouncedCalculate)
}
// 初始计算(延迟确保 DOM 渲染完成)
setTimeout(calculateHeight, 100)
// 监听窗口尺寸变化(兜底)
window.addEventListener('resize', debouncedCalculate)
})
// 清理
onUnmounted(() => {
window.removeEventListener('resize', debouncedCalculate)
if (debounceTimer) clearTimeout(debounceTimer)
})
return {
tableHeight,
recalculate: calculateHeight
}
}

View File

@ -0,0 +1,16 @@
import axios from 'axios'
const service = axios.create({
baseURL: '',
timeout: 10000
})
export async function request(config) {
const res = await service(config)
if (res === null || res === undefined) {
return res
}
if (Object.prototype.hasOwnProperty.call(res, 'data')) {
return res.data
}
}

View File

@ -1,49 +1,17 @@
<template>
<el-table :data="tableData" style="width: 100%" stripe height="100%">
<el-table-column prop="county" label="区县" width="80" align="center" />
<el-table-column
prop="road_code"
label="路线编码"
width="80"
align="center"
/>
<el-table-column
prop="mile_code"
label="里程桩号"
width="80"
align="center"
/>
<el-table-column
prop="level"
label="行政等级(仅划分:国道、省道、农村公路三类)"
width="180"
align="center"
/>
<el-table-column
prop="monthly_traffic"
label="机动车月交通流量"
align="center"
/>
<el-table-column prop="tec_level" label="技术等级" align="center" />
<el-table-column prop="name" label="服务保障点具体名称" align="center" />
<el-table-column prop="type" label="类型" align="center" />
<el-table-column
prop="service_station_address"
label="产权隶属单位(仅划分:交通公路部门含下属单位、其他部门或单位两类)"
width="180"
align="center"
/>
<el-table-column label="操作" fixed="right" align="center">
<template #default="scope">
<el-button size="small" @click="console.log(scope.row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
<DynamicTable
:dataSource="tableData"
:columns="columns"
:toolbar="toolbar"
:autoHeight="true"
:pagination="true"
></DynamicTable>
</template>
<script lang="ts" setup>
import DynamicTable from "../../component/DynamicTable";
import { h } from "vue";
import { request } from "@/utils/request";
const tableData = [
{
county: "潼南",
@ -146,6 +114,60 @@ const tableData = [
service_station_address: "交通公路部门",
},
];
const columns = [
{
prop: "county",
label: "区县",
},
{
prop: "road_code",
label: "路线编码",
// minWidth: 160,
},
{
prop: "mile_code",
label: "里程桩号",
},
{
prop: "level",
label: "行政等级(仅划分:国道、省道、农村公路三类)",
},
{
prop: "monthly_traffic",
label: "机动车月交通流量",
},
{
prop: "tec_level",
label: "技术等级",
},
{
prop: "name",
label: "服务保障点具体名称",
},
{
prop: "type",
label: "类型",
},
{
label: "操作",
fixed: "right",
width: 100,
render: (row) => {
return h(
ElButton,
{
type: "text",
on: {
click: () => {
console.log(row);
},
},
},
"详情"
);
},
},
];
</script>
<style lang="scss" scoped>

42
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@vueuse/core':
specifier: ^14.0.0
version: 14.0.0(vue@3.5.22)
dayjs:
specifier: ^1.11.18
version: 1.11.18
@ -23,7 +26,7 @@ importers:
version: 1.3.0
unplugin-auto-import:
specifier: ^0.17.0
version: 0.17.8(@vueuse/core@9.13.0(vue@3.5.22))(rollup@4.52.4)
version: 0.17.8(@vueuse/core@14.0.0(vue@3.5.22))(rollup@4.52.4)
unplugin-vue-components:
specifier: ^0.26.0
version: 0.26.0(@babel/parser@7.28.4)(rollup@4.52.4)(vue@3.5.22)
@ -54,7 +57,7 @@ importers:
version: 1.93.2
unplugin-auto-import:
specifier: ^0.17.0
version: 0.17.8(@vueuse/core@9.13.0(vue@3.5.22))(rollup@4.52.4)
version: 0.17.8(@vueuse/core@14.0.0(vue@3.5.22))(rollup@4.52.4)
unplugin-vue-components:
specifier: ^0.26.0
version: 0.26.0(@babel/parser@7.28.4)(rollup@4.52.4)(vue@3.5.22)
@ -521,6 +524,9 @@ packages:
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@vant/auto-import-resolver@1.3.0':
resolution: {integrity: sha512-lJyWtCyFizR4bHZvMiNMF3w+WTFTUWAvka1eqTnPK9ticUcKTCOx6qEmHcm8JPb3g1t3GaD2W3MnHkBp/nHamw==}
@ -571,12 +577,25 @@ packages:
'@vue/shared@3.5.22':
resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
'@vueuse/core@14.0.0':
resolution: {integrity: sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==}
peerDependencies:
vue: ^3.5.0
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@14.0.0':
resolution: {integrity: sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@14.0.0':
resolution: {integrity: sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@ -1397,6 +1416,8 @@ snapshots:
'@types/web-bluetooth@0.0.16': {}
'@types/web-bluetooth@0.0.21': {}
'@vant/auto-import-resolver@1.3.0': {}
'@vant/popperjs@1.3.0': {}
@ -1466,6 +1487,13 @@ snapshots:
'@vue/shared@3.5.22': {}
'@vueuse/core@14.0.0(vue@3.5.22)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.0.0
'@vueuse/shared': 14.0.0(vue@3.5.22)
vue: 3.5.22
'@vueuse/core@9.13.0(vue@3.5.22)':
dependencies:
'@types/web-bluetooth': 0.0.16
@ -1476,8 +1504,14 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/metadata@14.0.0': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@14.0.0(vue@3.5.22)':
dependencies:
vue: 3.5.22
'@vueuse/shared@9.13.0(vue@3.5.22)':
dependencies:
vue-demi: 0.14.10(vue@3.5.22)
@ -1951,7 +1985,7 @@ snapshots:
transitivePeerDependencies:
- rollup
unplugin-auto-import@0.17.8(@vueuse/core@9.13.0(vue@3.5.22))(rollup@4.52.4):
unplugin-auto-import@0.17.8(@vueuse/core@14.0.0(vue@3.5.22))(rollup@4.52.4):
dependencies:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.3.0(rollup@4.52.4)
@ -1962,7 +1996,7 @@ snapshots:
unimport: 3.14.6(rollup@4.52.4)
unplugin: 1.16.1
optionalDependencies:
'@vueuse/core': 9.13.0(vue@3.5.22)
'@vueuse/core': 14.0.0(vue@3.5.22)
transitivePeerDependencies:
- rollup