Files
BMS/bms-front/src/components/BasicTable/index.vue
T
2026-09-17 17:58:00 +08:00

460 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="basic-table">
<!-- 搜索栏 -->
<basic-form
v-if="searchColumns.length"
ref="searchFormRef"
:model-value="queryParams"
:schemas="searchColumns"
:rules="searchRules"
:label-width="searchLabelWidth"
:action-align="searchActionAlign"
:size="size"
inline
v-show="showSearch"
>
<template v-for="name in searchSlotNames" :key="name" #[toFormSlot(name)]>
<slot
:name="name"
:model="queryParams"
:item="props.searchColumns.find((item) => item.prop === name.slice('search-'.length))"
/>
</template>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<slot name="search-extra" />
</basic-form>
<!-- 工具栏 -->
<el-row v-if="showToolbar" :gutter="10" class="mb8">
<slot name="toolbar" :selection="selectionList" :ids="selectionIds" />
<el-col :span="1.5" v-if="!$slots.toolbar">
<el-button type="primary" plain icon="Plus" @click="$emit('add')" v-if="showAdd">新增</el-button>
</el-col>
<right-toolbar
v-model:showSearch="showSearchInternal"
:columns="toolbarColumns"
:search="searchColumns.length > 0"
@query-table="getList"
/>
</el-row>
<!-- 表格 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
:row-key="rowKey"
:border="border"
:stripe="stripe"
:size="size"
:height="height"
:max-height="maxHeight"
:default-sort="defaultSort"
:span-method="spanMethod"
@selection-change="handleSelectionChange"
@row-click="(row, col, e) => $emit('row-click', row, col, e)"
@sort-change="(c) => $emit('sort-change', c)"
>
<!-- 多选列 -->
<el-table-column v-if="selection" type="selection" width="55" align="center" />
<!-- 序号列 -->
<el-table-column v-if="showIndex" type="index" label="序号" width="55" align="center" />
<!-- 动态列 -->
<el-table-column
v-for="col in visibleColumns"
:key="col.key"
:prop="col.prop"
:label="col.label"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'center'"
:fixed="col.fixed"
:sortable="col.sortable"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
>
<template #header="scope">
<slot :name="`header-${col.prop}`" v-bind="scope">{{ col.label }}</slot>
</template>
<template #default="scope">
<slot :name="`col-${col.prop}`" v-bind="scope" :row="scope.row">
<!-- 字典列 -->
<dict-tag
v-if="col.dict"
:options="getDictOptions(col.dict)"
:value="scope.row[col.prop]"
/>
<!-- 时间格式化 -->
<span v-else-if="col.parseTime">{{ parseTime(scope.row[col.prop], col.parseTime === true ? undefined : col.parseTime) }}</span>
<!-- 自定义格式化 -->
<span v-else-if="col.formatter">{{ col.formatter(scope.row, col, scope.row[col.prop], scope.$index) }}</span>
<!-- 默认显示 -->
<span v-else>{{ scope.row[col.prop] }}</span>
</slot>
</template>
</el-table-column>
<!-- 空数据 -->
<template #empty>
<slot name="empty"><el-empty /></slot>
</template>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:page-sizes="pageSizes"
@pagination="getList"
/>
</div>
</template>
<script setup>
/**
* 公共表格组件 BasicTable
* 集成搜索栏 + 工具栏 + 表格 + 分页,基于 columns/searchColumns 配置驱动,内置复用 BasicForm、RightToolbar、Pagination、DictTag。
*
* 用法示例:
* <basic-table
* ref="tableRef"
* :columns="columns"
* :search-columns="searchColumns"
* :query="listPost"
* @add="handleAdd"
* >
* <template #toolbar="{ selection, ids }">
* <el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
* </template>
* <template #col-status="{ row }">
* <dict-tag :options="sys_normal_disable" :value="row.status" />
* </template>
* </basic-table>
*
* column 配置:
* {
* label: '岗位名称', // 列标题
* prop: 'postName', // 字段名
* width: 120, // 列宽
* minWidth: 120,
* align: 'center', // 对齐(默认 center)
* fixed: 'left'|'right',
* sortable: true,
* showOverflowTooltip: true, // 默认 true
* dict: 'sys_normal_disable', // 用字典标签渲染
* parseTime: true | 'YYYY-MM-DD', // 时间格式化(true 用默认格式)
* formatter: (row,col,val,index)=> ..., // 自定义格式化
* show: true, // 是否显示(参与显隐列)
* showInToolbar: true, // 是否在显隐列工具栏中显示(默认 true)
* visible: true // 初始是否可见(显隐列控制)
* }
*
* searchColumns 配置:同 BasicForm 的 schemas。
*/
defineOptions({ name: 'BasicTable' })
const props = defineProps({
// 列配置数组
columns: {
type: Array,
default: () => []
},
// 搜索表单配置(同 BasicForm schemas)
searchColumns: {
type: Array,
default: () => []
},
// 搜索表单校验规则
searchRules: {
type: Object,
default: () => ({})
},
// 搜索表单标签宽度
searchLabelWidth: {
type: [String, Number],
default: 'auto'
},
// 搜索按钮区对齐:left / center(搜索项较多按钮换行时可用 center 让按钮居中)
searchActionAlign: {
type: String,
default: 'left'
},
// 列表加载函数:(params) => Promise<{rows, total}> | {rows, total}
query: {
type: Function,
required: true
},
// 额外固定查询参数(每次请求合并传入,不参与搜索表单重置)
extraParams: {
type: Object,
default: () => ({})
},
// 初始查询参数(搜索字段的默认值)
defaultParams: {
type: Object,
default: () => ({})
},
// 是否显示多选列
selection: {
type: Boolean,
default: true
},
// 是否显示序号列
showIndex: {
type: Boolean,
default: false
},
// 是否显示工具栏
showToolbar: {
type: Boolean,
default: true
},
// 是否显示默认新增按钮(无 toolbar 插槽时生效)
showAdd: {
type: Boolean,
default: false
},
// 是否显示搜索栏(可双向控制)
showSearch: {
type: Boolean,
default: true
},
// 行 key
rowKey: {
type: [String, Function],
default: undefined
},
// 边框
border: {
type: Boolean,
default: false
},
// 斑马纹
stripe: {
type: Boolean,
default: false
},
// 表格/表单尺寸(large/default/small)
size: {
type: String,
default: undefined
},
// 表格高度
height: { type: [String, Number], default: undefined },
maxHeight: { type: [String, Number], default: undefined },
// 默认排序(透传 el-table default-sort,如 { prop: 'createTime', order: 'descending' })
defaultSort: {
type: Object,
default: undefined
},
// 单元格合并方法(透传 el-table span-method),签名同 el-table:({ row, column, rowIndex, columnIndex }) => { rowspan, colspan }
spanMethod: {
type: Function,
default: null
},
// 每页条数可选项
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50]
},
// 默认每页条数
pageSize: {
type: Number,
default: 10
},
// 是否在挂载后自动加载
autoLoad: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['add', 'update:showSearch', 'row-click', 'sort-change', 'selection-change'])
const { proxy } = getCurrentInstance()
const tableRef = ref()
const searchFormRef = ref()
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const selectionList = ref([])
const showSearchInternal = ref(props.showSearch)
// 查询参数:搜索字段 + 分页
const queryParams = reactive({
pageNum: 1,
pageSize: props.pageSize,
...props.defaultParams
})
watch(() => props.showSearch, (v) => (showSearchInternal.value = v))
watch(showSearchInternal, (v) => emit('update:showSearch', v))
// ====== 列显隐状态 ======
// 内部维护列状态(带 key/visible),供 RightToolbar 控制显隐
const tableColumns = ref([])
function initColumns() {
tableColumns.value = props.columns.map((col, index) => ({
...col,
key: col.key || col.prop || index,
visible: col.visible !== false && col.show !== false
}))
}
initColumns()
watch(
() => props.columns,
() => initColumns(),
{ deep: false }
)
// 实际渲染的列(visible !== false)
const visibleColumns = computed(() => tableColumns.value.filter((c) => c.visible !== false))
// 参与显隐工具栏的列(有 label 且未禁用工具栏显示)
const toolbarColumns = computed(() =>
tableColumns.value.filter((c) => c.label && c.showInToolbar !== false)
)
// ====== 字典加载(用于表格列 dict 渲染) ======
const dictMap = reactive({})
const dictTypes = computed(() => {
const set = new Set()
props.columns.forEach((c) => c.dict && set.add(c.dict))
props.searchColumns.forEach((c) => c.dict && set.add(c.dict))
return [...set]
})
watch(
dictTypes,
(types) => {
if (!types.length) return
const dictRefs = proxy.useDict(...types)
types.forEach((t) => {
const d = dictRefs[t]
dictMap[t] = isRef(d) ? d : ref(d)
})
},
{ immediate: true }
)
function getDictOptions(type) {
const d = dictMap[type]
return isRef(d) ? d.value : d || []
}
// ====== 搜索栏插槽透传:search-xxx -> form-xxx ======
const slots = useSlots()
const searchSlotNames = computed(() =>
Object.keys(slots).filter((n) => n.startsWith('search-') && n !== 'search-extra')
)
function toFormSlot(name) {
return 'form-' + name.slice('search-'.length)
}
// ====== 数据加载 ======
async function getList() {
loading.value = true
try {
const params = { ...queryParams, ...props.extraParams }
const res = await props.query(params)
tableData.value = res?.rows || []
total.value = res?.total || 0
} catch (e) {
console.error('BasicTable 加载失败', e)
} finally {
loading.value = false
}
}
// 搜索
function handleQuery() {
queryParams.pageNum = 1
getList()
}
// 重置搜索
function resetQuery() {
searchFormRef.value?.resetFields()
Object.keys(props.defaultParams || {}).forEach((k) => {
queryParams[k] = props.defaultParams[k]
})
queryParams.pageNum = 1
getList()
}
// 选中变化
function handleSelectionChange(val) {
selectionList.value = val
emit('selection-change', val)
}
const selectionIds = computed(() =>
selectionList.value.map((r) => (props.rowKey ? r[props.rowKey] : r.id)).filter((v) => v !== undefined)
)
// ====== 暴露方法 ======
function refresh() {
getList()
}
function reload() {
queryParams.pageNum = 1
getList()
}
function getSelection() {
return selectionList.value
}
function getSelectionIds() {
return selectionIds.value
}
function clearSelection() {
tableRef.value?.clearSelection()
}
function toggleRowSelection(row, selected) {
tableRef.value?.toggleRowSelection(row, selected)
}
function getTableRef() {
return tableRef.value
}
function getSearchFormRef() {
return searchFormRef.value
}
function getQueryParams() {
return { ...queryParams, ...props.extraParams }
}
function setQueryParams(p) {
Object.assign(queryParams, p)
}
defineExpose({
refresh,
reload,
getList,
handleQuery,
resetQuery,
getSelection,
getSelectionIds,
clearSelection,
toggleRowSelection,
getTableRef,
getSearchFormRef,
getQueryParams,
setQueryParams,
getData: () => tableData.value,
/** 当前页数据(响应式,父组件 span-method 可直接订阅) */
data: tableData
})
if (props.autoLoad) {
onMounted(getList)
}
</script>
<style scoped>
.basic-table {
width: 100%;
}
</style>