460 lines
12 KiB
Vue
460 lines
12 KiB
Vue
<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>
|