You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
7.5 KiB
Vue
270 lines
7.5 KiB
Vue
<template>
|
|
<Dialog :title="title" v-model="dialogVisible" :appendToBody="true" width="1080">
|
|
<ContentWrap>
|
|
<!-- 新增查询表单区域 -->
|
|
<div v-if="$slots.header" class="search-container">
|
|
<slot name="header"></slot>
|
|
</div>
|
|
|
|
<el-table
|
|
ref="tableRef"
|
|
v-loading="loading"
|
|
:data="list"
|
|
:row-key="resolveRowKey"
|
|
:show-overflow-tooltip="true"
|
|
:stripe="true"
|
|
@selection-change="handleSelectionChange"
|
|
@row-click="handleRowClick"
|
|
>
|
|
<el-table-column
|
|
type="selection"
|
|
width="55"
|
|
reserve-selection
|
|
:selectable="isSelectionColumnSelectable"
|
|
/>
|
|
<el-table-column
|
|
v-for="column in columns"
|
|
:key="column.prop || column.slotName"
|
|
:label="column.label"
|
|
:prop="column.slotName ? undefined : column.prop"
|
|
:width="column.width"
|
|
:min-width="column.minWidth"
|
|
:align="column.align || 'center'"
|
|
:fixed="column.fixed"
|
|
>
|
|
<template v-if="column.slotName" #default="scope">
|
|
<slot :name="column.slotName" :row="scope.row"></slot>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<Pagination
|
|
v-model:page="queryParams.pageNo"
|
|
v-model:limit="queryParams.pageSize"
|
|
:total="total"
|
|
@pagination="getList"
|
|
/>
|
|
</ContentWrap>
|
|
<template #footer>
|
|
<el-button type="primary" @click="submitSelection">确 定</el-button>
|
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ElTable } from 'element-plus'
|
|
|
|
defineOptions({ name: 'TableSelectDialog' })
|
|
|
|
type TableColumn = {
|
|
label: string
|
|
prop?: string
|
|
slotName?: string
|
|
width?: string | number
|
|
minWidth?: string | number
|
|
align?: 'left' | 'center' | 'right'
|
|
fixed?: string | boolean
|
|
}
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
title: string
|
|
columns: TableColumn[]
|
|
fetchApi: (params: Record<string, any>) => Promise<{ list: any[]; total: number }>
|
|
selectionType?: 'single' | 'multiple'
|
|
rowKey?: string
|
|
pageSize?: number
|
|
initialRows?: any[]
|
|
queryParams?: Record<string, any> // 新增
|
|
selectable?: (row: any, index: number) => boolean
|
|
/** ✅ 新增 */
|
|
defaultSelectedKeys?: (string | number)[]
|
|
}>(),
|
|
{
|
|
selectionType: 'multiple',
|
|
rowKey: 'id',
|
|
pageSize: 10,
|
|
initialRows: () => [],
|
|
queryParams: () => ({}), // 默认值
|
|
defaultSelectedKeys: () => []
|
|
}
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'confirm', value: { ids: (number | string)[]; rows: any[] }): void
|
|
}>()
|
|
|
|
const dialogVisible = ref(false)
|
|
const loading = ref(false)
|
|
const list = ref<any[]>([])
|
|
const total = ref(0)
|
|
const tableRef = ref<InstanceType<typeof ElTable>>()
|
|
const queryParams = reactive({
|
|
pageNo: 1,
|
|
pageSize: props.pageSize
|
|
})
|
|
const selectedMap = ref(new Map<number | string, any>())
|
|
const syncingSelection = ref(false)
|
|
const isSingleSelect = computed(() => props.selectionType === 'single')
|
|
|
|
const resolveRowKey = (row: Record<string, any>) => row[props.rowKey]
|
|
const isRowSelectable = (row: any, index = list.value.findIndex((item) => resolveRowKey(item) === resolveRowKey(row))) => {
|
|
return props.selectable ? props.selectable(row, index) : true
|
|
}
|
|
const isSelectionColumnSelectable = (row: any, index: number) => isRowSelectable(row, index)
|
|
|
|
const removeUnselectableSelections = () => {
|
|
list.value.forEach((row, index) => {
|
|
if (!isRowSelectable(row, index)) {
|
|
selectedMap.value.delete(resolveRowKey(row))
|
|
}
|
|
})
|
|
}
|
|
|
|
const refreshSelectionOnTable = async () => {
|
|
await nextTick()
|
|
const table = tableRef.value
|
|
if (!table) return
|
|
syncingSelection.value = true
|
|
try {
|
|
table.clearSelection()
|
|
const selectedIds = selectedMap.value
|
|
list.value.forEach((row, index) => {
|
|
const rowId = resolveRowKey(row)
|
|
if (selectedIds.has(rowId) && isRowSelectable(row, index)) {
|
|
table.toggleRowSelection(row, true)
|
|
}
|
|
})
|
|
} finally {
|
|
syncingSelection.value = false
|
|
}
|
|
}
|
|
/**
|
|
* ✅ 新增:根据 defaultSelectedKeys 初始化选中状态
|
|
*/
|
|
const initDefaultSelection = () => {
|
|
if (!props.defaultSelectedKeys.length) return
|
|
|
|
props.defaultSelectedKeys.forEach((key) => {
|
|
selectedMap.value.set(key, { [props.rowKey]: key })
|
|
})
|
|
}
|
|
|
|
const getList = async () => {
|
|
loading.value = true
|
|
try {
|
|
const params = getQueryParams()
|
|
const data = await props.fetchApi(params)
|
|
list.value = data.list || []
|
|
total.value = data.total || 0
|
|
removeUnselectableSelections()
|
|
await refreshSelectionOnTable()
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleSelectionChange = (rows: any[]) => {
|
|
if (syncingSelection.value) return
|
|
const selectableRows = rows.filter((row) => isRowSelectable(row))
|
|
if (isSingleSelect.value) {
|
|
selectedMap.value.clear()
|
|
const latestRow = selectableRows.length ? selectableRows[selectableRows.length - 1] : undefined
|
|
if (latestRow) {
|
|
selectedMap.value.set(resolveRowKey(latestRow), latestRow)
|
|
}
|
|
void refreshSelectionOnTable()
|
|
return
|
|
}
|
|
const pageIdSet = new Set(list.value.map((item) => resolveRowKey(item)))
|
|
pageIdSet.forEach((id) => {
|
|
selectedMap.value.delete(id)
|
|
})
|
|
selectableRows.forEach((row) => {
|
|
selectedMap.value.set(resolveRowKey(row), row)
|
|
})
|
|
}
|
|
|
|
const handleRowClick = (row: any) => {
|
|
if (!isSingleSelect.value) return
|
|
if (!isRowSelectable(row)) return
|
|
const table = tableRef.value
|
|
if (!table) return
|
|
syncingSelection.value = true
|
|
try {
|
|
table.clearSelection()
|
|
table.toggleRowSelection(row, true)
|
|
} finally {
|
|
syncingSelection.value = false
|
|
}
|
|
selectedMap.value.clear()
|
|
selectedMap.value.set(resolveRowKey(row), row)
|
|
}
|
|
|
|
const open = async (rows?: any[]) => {
|
|
/* selectedMap.value.clear()
|
|
const initialRows = rows || props.initialRows
|
|
const nextRows = isSingleSelect.value ? initialRows.slice(0, 1) : initialRows
|
|
nextRows.forEach((row) => {
|
|
selectedMap.value.set(resolveRowKey(row), row)
|
|
})
|
|
queryParams.pageNo = 1
|
|
queryParams.pageSize = props.pageSize
|
|
dialogVisible.value = true
|
|
await getList()*/
|
|
selectedMap.value.clear()
|
|
|
|
// ✅ 优先使用 defaultSelectedKeys
|
|
if (props.defaultSelectedKeys.length) {
|
|
initDefaultSelection()
|
|
}
|
|
|
|
// ✅ 兼容原来的 initialRows / rows
|
|
const initialRows = rows || props.initialRows
|
|
const nextRows = isSingleSelect.value ? initialRows.slice(0, 1) : initialRows
|
|
nextRows.forEach((row) => {
|
|
selectedMap.value.set(resolveRowKey(row), row)
|
|
})
|
|
|
|
queryParams.pageNo = 1
|
|
queryParams.pageSize = props.pageSize
|
|
dialogVisible.value = true
|
|
await getList()
|
|
}
|
|
|
|
const submitSelection = () => {
|
|
const rows = Array.from(selectedMap.value.values())
|
|
const ids = rows.map((row) => resolveRowKey(row))
|
|
emit('confirm', { ids, rows })
|
|
dialogVisible.value = false
|
|
}
|
|
|
|
// 暴露给父组件的方法
|
|
const reload =async () => {
|
|
loading.value = true
|
|
// 这里是你组件内部原有的加载数据逻辑
|
|
await getList()
|
|
}
|
|
|
|
// 3. 合并参数:将父组件传入的 queryParams 与内部参数合并
|
|
const getQueryParams = () => {
|
|
return {
|
|
pageNo: queryParams.pageNo || 1,
|
|
pageSize: queryParams.pageSize || 10,
|
|
...props.queryParams // 父组件传入的查询条件
|
|
}
|
|
}
|
|
// 4. 监听父组件参数变化
|
|
watch(() => props.queryParams, async () => {
|
|
await getList()
|
|
}, { deep: true })
|
|
|
|
defineExpose({ open, reload })
|
|
</script>
|
|
<style scoped>
|
|
.search-container {
|
|
margin-bottom: 10px;
|
|
padding: 0 0px;
|
|
}
|
|
</style>
|