基于vue3和elementplus实现的自定义table组件
基于vue3和elementplus实现的自定义table组件,集成查询工具栏分页,可通过配置直接实现基础的列表页
效果预览:
目录结构如下:
类型声明:type/table.d.ts
declare type DictType = {
value: string | boolean | number
label: string
type?: string
}
/**
* table传入column的配置项
* 注:selection,多选配置下接口和elementPlus接口一致
* @param label:名称
* @param key:key值,key值不可以使用type属性规定几个关键字
* @param type: element Table-column type属性 selection / index / expand / operation / link / tag ( 注意:暂只支持使用index,selection,operation(暂时无内置按钮,需自己slot传入))
* @param format:表格回显格式化函数
* @param onlyTable: 是否只在表格中显示
* @param onlySearch: 是否只在查询中显示
* @param searchFormatDate: 查询中含有日期选择器时的格式化
* @param searchType:查询条件以哪种方式展示,暂时支持input,select,tree(tree多选)以及date下的type( 'year','month','date','datetime','week','datetimerange','daterange')等
* @param searchKey: 查询参数表单key值,不传默认使用key
* @param searchOptions: 当type为input-select时需要传下拉配置项,input-select目前只支持一个,不支持多个,也就是一个列表查询只能有一个input-select
* @param dict:字典,searchType为select需要填写
* @param unit: 单位,可设置单位添加在单元格数据后面
* @param btnConfig: type=operation时的操作按钮配置
* @param sortable: 开启列排序,默认不开启列排序,遵循element-plus table sortable规则
* @param children: 多级表头
*/
declare type ColumnProps = {
label?: string
key: string
type?: 'selection' | 'index' | 'expand' | 'operation' | 'link' | 'tag'
linkClick?: any
format?: any
onlyTable?: boolean
valueWidth?: string
labelWidth?: string
onlySearch?: boolean
searchFormatDate?: string
searchType?:
| 'input'
| 'input-select'
| 'select'
| 'select-multiple'
| 'tree'
| 'tree-strictly'
| IDatePickerType
searchKey?: string | string[]
searchDefaultValue?: any
searchOptions?: any
dict?: Array<DictType>
unit?: string
btnConfig?: Array<Operation>
sortable?: boolean | 'custom'
width?: string | number
fixed?: true | 'left' | 'right'
children?: Array<ColumnProps>
}
/**
* 自定义表格查询组件
* @description 注意:默认超出隐藏,在使用table-column插槽自定义内容时,不要使用div,可使用span替代,不然不会显示省略号
* @param rowKey: 行key值,开启多选必填
* @param labelWidth: 查询表单label宽度
* @param searchAble: 是否需要查询
* @param api: 请求数据方法名(需在api目录中什么请求接口)
* @param tableConfig: table配置
* @param optBtnCfg: 操作按钮配置
* @param hasOptOrToolBtnCfg?: 是否需要操作或者工具栏
* @param hasPagination?: 是否需要分页
* @param toolBtnCfg: 工具按钮配置
*/
declare type CustomTable = {
rowKey?: string
expandRow: any
labelWidth?: string
valueWidth?: string
searchAble?: boolean
api: string
tableConfig: ColumnProps[]
hasOptOrToolBtnCfg?: boolean
hasPagination?: boolean
optBtnCfg?: any
toolBtnCfg?: any
queryParam?: object
}
/**
* 分页
*/
declare type PageRefType = {
HTMLElement
pageSize: number
currentPage: number
reset: any
returnPage: any
}
/**
* @param type: 内置类型"detail" | "delete" | "edit" | "download"
* @param key: 必传,用于指定所需值的key(id)值
* @param path: 跳转页面需要传
* @param api: 删除操作等需要调用接口的按钮需要传
* @param permission: 权限,可以传字符串或者布尔值,字符串为权限buttonPermission中的key值,如果传布尔值,则根据布尔值判断权限
* @param break: 切断按钮点击事件,并提示,如果希望阻止提交并给出提示信息,可用该属性
*/
declare type Operation = {
type: 'detail' | 'delete' | 'download' | 'edit'
// 跳转页面需要传
path?: string
// 必传,用于指定所需值的key(id)值
key?: string
// 删除操作等需要调用接口的按钮需要传
api?: string
// 权限,可以传字符串或者布尔值,字符串为权限buttonPermission中的key值,如果传布尔值,则根据布尔值判断权限
permission?: string | boolean
// 切断按钮点击事件,并提示,如果希望阻止提交并给出提示信息,可用该属性
break?: string
// 是否展示按钮的判断函数
showFn?: any
[key]?: any
}
declare type OptType =
| 'add'
| 'edit'
| 'delete-all'
| 'delete-select'
| 'export'
declare type ToolType = 'refresh' | 'printer' | 'operation' | 'search'
declare type OptBtnCfg = {
type: OptType
api?: string
}
declare type ToolBtnCfg = {
type: ToolType
api?: string
}
index.vue
<template>
<div ref="tableList" class="table-list" style="width: 100%">
<SearchForm
ref="searchForm"
v-if="searchAble"
:label-width="labelWidth"
:valueWidth="valueWidth"
:query-param="queryParam"
:search-from-config="searchFromConfig"
@condition-change="conditionChange"
/>
<div class="table-center" v-if="hasOptOrToolBtnCfg">
<slot name="opt-btn">
<OptionsBtn
v-if="optBtnCfg.length > 0"
:config="optBtnCfg"
:params="condForm"
:total="total"
/>
</slot>
<slot name="tool-btn">
<ToolBtn
v-if="toolBtnCfg.length > 0"
:config="toolBtnCfg"
:params="condForm"
:total="total"
/>
</slot>
</div>
<el-table
ref="cusElTable"
:data="tableData"
:expand-row-keys="expandRow"
:row-key="rowKey"
style="width: 100%"
@expand-change="expandChange"
@row-click="rowClick"
@select="select"
@select-all="selectAll"
@selection-change="selectionChange"
@sort-change="sortChange"
v-loading="loading"
show-overflow-tooltip
>
<template v-for="column in tableConfig" :key="column.key">
<el-table-column
v-if="column.type === 'expand' || column.key === 'expand'"
:fixed="column.fixed"
type="expand"
:width="column.width || 44"
:reserve-selection="true"
>
<template #default="scope">
<!-- 提供默认插槽 -->
<slot name="expand" :scope="scope.row">
<el-empty
description="暂无数据"
:image-size="0"
style="padding: 10px"
/>
</slot>
</template>
</el-table-column>
<el-table-column
v-else-if="column.type === 'selection' || column.key === 'selection'"
:fixed="column.fixed"
:label="column.label"
type="selection"
:width="column.width || 44"
/>
<el-table-column
v-else-if="column.type === 'index' || column.key === 'index'"
:fixed="column.fixed"
:label="column.label"
type="index"
:width="column.width || 55"
/>
<el-table-column
v-else-if="column.type === 'operation' || column.key === 'operation'"
fixed="right"
label="操作"
:width="column.width || 120"
>
<template #default="scope">
<!-- 提供默认插槽 -->
<slot name="operation" :scope="scope.row">
<template v-for="operation in column.btnConfig">
<el-button
v-if="
hasPermission(operation, 'detail') &&
hasShowBtn(operation, scope.row)
"
:key="operation.type"
link
size="small"
type="primary"
@click="toDetail(scope.row, operation)"
>
详情
</el-button>
<el-button
v-if="
hasPermission(operation, 'edit') &&
hasShowBtn(operation, scope.row)
"
:key="operation.type"
link
size="small"
type="primary"
@click="editItem(scope.row, operation)"
>
编辑
</el-button>
<el-button
v-if="
hasPermission(operation, 'download') &&
hasShowBtn(operation, scope.row)
"
:key="operation.type"
link
size="small"
type="primary"
@click="downloadFile(scope.row, operation)"
>
下载附件
</el-button>
<el-popconfirm
v-if="
hasPermission(operation, 'delete') &&
hasShowBtn(operation, scope.row)
"
title="是否删除?"
:key="operation.type"
@confirm="deleteItem(scope.row, operation)"
>
<template #reference>
<el-button link size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</slot>
</template>
</el-table-column>
<TableColumn
v-else-if="!column.onlySearch"
:column="column"
:params="condForm"
>
<template #[column.key]="scope">
<slot :name="column.key" :scope="scope.scope"></slot>
</template>
</TableColumn>
</template>
</el-table>
<!--分页查询工具条-->
<TablePagination
v-if="hasPagination"
ref="cusPage"
:total="total"
@page-change="handlePageChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, PropType, watch } from 'vue'
import SearchForm from './SearchForm.vue'
import TablePagination from './TablePagination.vue'
import TableColumn from './TableColumn.vue'
import ToolBtn from './ToolBtn.vue'
import OptionsBtn from './OptionsBtn.vue'
import Api from '@/api/index.ts'
import { sysApi, abTools } from '~/agilebpm'
import { useRouter } from 'vue-router'
const router = useRouter()
import { useUserStore } from '@/store/modules/user'
import { useAclStore } from '@/store/modules/acl'
const userStore = useUserStore()
const aclStore = useAclStore()
const props: CustomTable = defineProps({
/**
*@description rowKey: 行数据key值
*/
rowKey: {
type: String,
default: 'id',
},
// 默认展开行
expandRow: { type: Array, default: () => [] },
/**
* 是否需要查询,默认true
*/
searchAble: {
type: Boolean,
default: true,
},
/**
* 是否需要中间操作按钮,默认true
*/
hasOptOrToolBtnCfg: {
type: Boolean,
default: true,
},
/**
* 是否需要分页,默认true
*/
hasPagination: {
type: Boolean,
default: true,
},
labelWidth: {
type: String,
default: '100px',
},
valueWidth: {
type: String,
default: '250px',
},
api: {
type: String,
default: '',
required: true,
},
tableConfig: {
type: Array as PropType<ColumnProps[]>,
default: () => [],
required: true,
},
optBtnCfg: {
type: Array as PropType<OptBtnCfg[]>,
default: () => [],
},
toolBtnCfg: {
type: Array as PropType<ToolBtnCfg[]>,
default: () => [],
},
// 默认查询参数
queryParam: {
type: Object,
required: false,
default: () => {},
},
})
const loading = ref(false)
const cusElTable = ref<any>()
const tableList = ref<HTMLElement>()
const typeEum: any = ['selection', 'index', 'expand', 'operation']
const searchFromConfig = computed(() => {
return props.tableConfig.filter(
(el) => !el.onlyTable && !typeEum.includes([el?.type])
)
})
// 是否存在附件
const hasShowBtn = (e: any, row: any) => {
if (e.showFn) {
return e.showFn(row)
}
return true
}
// 是否有权限
const hasPermission = computed(() => {
return (e: any, key: string) => {
if ('permission' in e) {
if (typeof e.permission === 'boolean') {
return e.type === key && e.permission
} else if (typeof e.permission === 'string') {
return e.type === key && aclStore.buttonPermission[e.permission]
} else {
// 预留其他情况
}
}
return e.type === key
}
})
// 构建条件查询form,没有申明searchKey,使用key作为表单属性
const condForm = ref({})
onMounted(async () => {
await nextTick()
getTableList()
})
const tableData = ref([])
const conditionChange = (form: any) => {
// console.log('form-l;;;', form)
// if (props.queryParam) {
// const info: any = props.queryParam
// Object.keys(info).forEach((el) => {
// if (!form[el]) {
// form[el] = info[el]
// }
// })
// }
// condForm.value = form
condForm.value = { ...form, ...props.queryParam }
cusPage?.value?.reset()
getTableList()
}
//下载附件
const downloadFile = async (doc: any, e: any) => {
if (e.break) {
ElMessage.error(e.break)
} else {
//导出上传的文件
if (e.key) {
if (doc[e.key] && doc[e.key].length > 0) {
const filesArray = JSON.parse(doc[e.key])
let errNames = ''
for (const i in filesArray) {
const file = filesArray[i]
try {
const rel = await abTools.downBySteam(file.id, file.name)
if (!rel) {
errNames += errNames ? file.name : `,${file.name}`
}
if (errNames) {
ElMessage.error(
`下载失败!文件中包含不存在的文件【${errNames}】,请尝试单独下载`
)
} else {
ElMessage.success('下载成功')
}
} catch {}
}
}
} else {
throw '当type为download时,请指出要下载的附件字段,即需要提供key字段'
}
}
}
//编辑
const editItem = (scope: any, cfg: any) => {
if (cfg.break) {
ElMessage.error(cfg.break)
} else {
if (cfg.path.includes('?')) {
router.push(cfg.path + '&id=' + scope[cfg.key])
} else {
router.push(cfg.path + '?id=' + scope[cfg.key])
}
}
}
//删除
const deleteItem = async (scope: any, key: any) => {
if (key.break) {
ElMessage.error(key.break)
} else {
if (key.api) {
const apiParam = key.api.split('.')
let Fn: any = Api
apiParam.forEach((el: any) => {
Fn = Fn[el]
})
const _data = await Fn(scope[key.key])
if (_data.isOk) {
ElMessage.success('删除成功')
getTableList()
}
} else {
throw '当type为delete时,请提供api'
}
}
}
// 详情
const toDetail = (scope: any, cfg: any) => {
if (cfg.break) {
ElMessage.error(cfg.break)
} else {
if (cfg.flowKey) {
router.push(
`/bpm/bpm/instanceDetail?instId=${
cfg.flowKey ? scope[cfg.flowKey] : ''
}`
)
} else if (cfg.key) {
if (cfg.path) {
if (cfg.path.includes('?')) {
router.push(cfg.path + '&id=' + scope[cfg.key])
} else {
router.push(cfg.path + '?id=' + scope[cfg.key])
}
}
throw '当type为detail,如果是流程,flowKey是必填项,如果不是流程,path和key是必填项'
}
}
}
// 分页相关
const total = ref(0)
const cusPage: any = ref<PageRefType>()
const selectedDataOld: any = ref([])
const handlePageChange = (e: any) => {
// TODO 查询
if (selectedData.value[e.currentPage - 1]) {
selectedDataOld.value = JSON.parse(
JSON.stringify(selectedData.value[e.currentPage - 1])
)
}
getTableList()
}
const getTableList = async (prop?: string, order?: 'DESC' | 'ASC') => {
loading.value = true
const params = JSON.parse(JSON.stringify(condForm.value))
Object.keys(condForm.value).forEach((element) => {
if (element.split(',').length > 1) {
element.split(',').forEach((el, index) => {
params[el] = condForm.value[element]
? condForm.value[element][index]
: ''
})
delete params[element]
}
})
// 删除为空的条件
Object.keys(params).forEach((el) => {
if (!params[el] && typeof params[el] !== 'boolean' && params[el] !== 0) {
delete params[el]
}
})
const info = {
offset: cusPage?.value
? cusPage?.value?.pageSize * (cusPage?.value?.currentPage - 1)
: 0,
limit: cusPage?.value ? cusPage?.value?.pageSize : 10,
sortColumn: prop ? prop : '',
sortOrder: order ? order : '',
enablePage: props.hasPagination,
searchCount: true,
queryParam: {
...params,
},
}
const apiParam = props.api.split('.')
let Fn: any = Api
apiParam.forEach((el) => {
Fn = Fn[el]
})
const _data = await Fn(info)
tableData.value = _data.data.rows
total.value = _data.data.total
await nextTick()
if (selectedDataOld.value.length > 0) {
tableData.value.forEach((element: any) => {
let ids = selectedDataOld.value.findIndex(
(el: any) => el.id === element.id
)
if (ids !== -1) {
toggleRowSelection(element, true)
}
})
}
loading.value = false
}
const searchForm = ref()
watch(
() => props.queryParam,
(newVal, oldVal) => {
condForm.value = { ...condForm.value, ...newVal }
searchForm.value.optionClick('reset')
// cusPage?.value?.reset()
// getTableList()
},
{ deep: true }
)
// 排序
const sortChange = ({ column, prop, order }: any) => {
//prop:name, order: 'ascending' 'descending'
const _order =
order === 'ascending'
? 'ASC'
: order === 'descending'
? 'DESC'
: undefined
getTableList(prop, _order)
}
const emit = defineEmits([
'select',
'select-all',
'selection-change',
'row-click',
'expand-click',
])
// 多选相关方法
// 已经选择的选项
const selectedData: any = ref([])
const select = (selection: any, row: any) => {
// console.log('select', selection, row)
emit('select', selection, row)
}
const selectAll = (selection: any) => {
// console.log('selectAll', selection)
emit('select-all', selection)
}
const selectionChange = (selection: any) => {
// 页面选中的map
selectedData.value[cusPage?.value?.currentPage - 1] = selection
emit('selection-change', selection, selectedData.value)
}
const rowClick = (row: any, column: any, event: any) => {
emit('row-click', row, column, event)
}
const expandChange = (row: any, expand: any) => {
emit('expand-click', row, expand)
}
const clearSelection = () => {
cusElTable.value.clearSelection()
}
const toggleRowSelection = (row: any, selected: any) => {
cusElTable.value.toggleRowSelection(row, selected)
}
const toggleAllSelection = () => {
cusElTable.value.toggleAllSelection()
}
const toggleRowExpansion = (row: any, expanded: any) => {
cusElTable.value.toggleRowExpansion(row, expanded)
}
const setCurrentRow = (row: any) => {
cusElTable.value.setCurrentRow(row)
}
const clearSort = () => {
cusElTable.value.clearSort()
}
const refresh = () => {
selectedData.value = []
selectedDataOld.value = []
cusPage?.value?.reset()
getTableList()
}
onUnmounted(() => {
cusPage?.value?.reset()
selectedData.value = []
selectedDataOld.value = []
})
defineExpose({
clearSelection,
toggleRowSelection,
toggleAllSelection,
toggleRowExpansion,
setCurrentRow,
clearSort,
refresh,
})
</script>
<style lang="scss">
.btn-row {
width: 100%;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.table-center {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.table-list {
.el-empty__image {
display: none;
}
.el-empty__description {
margin-top: 0px;
}
}
</style>
OptionsBtn.vue
<template>
<div style="display: flex; margin-bottom: 8px">
<template v-for="(item, index) in config" :key="index">
<el-button
v-if="item.type === 'add'"
type="success"
@click="handleClick(item)"
>
新增
</el-button>
<el-button
v-if="item.type === 'edit'"
type="warning"
@click="handleClick(item)"
>
修改
</el-button>
<el-button
v-if="item.type === 'delete-all'"
type="danger"
@click="handleClick(item)"
>
全部删除
</el-button>
<el-button
v-if="item.type === 'delete-select'"
type="danger"
@click="handleClick(item)"
>
批量删除
</el-button>
<el-button
v-if="item.type === 'export'"
type="primary"
@click="handleExportClick(item)"
>
批量导出
</el-button>
</template>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, PropType, watch } from 'vue'
import AttendanceApi from '@/api/attendance'
import { downLoadFile } from '~/src/utils'
import { split } from 'lodash'
const props = defineProps({
config: {
type: Array as PropType<OptBtnCfg[]>,
default: () => [],
},
params: {
type: Object,
default: () => {},
},
total: {
type: Number,
default: 0,
},
})
const queryParams = ref({})
// 查询条件form
watch(
() => props.params,
(newVal) => {
if (props.params) {
const info = JSON.parse(JSON.stringify(props.params))
Object.keys(props.params).forEach((element) => {
if (element.split(',').length > 1) {
element.split(',').forEach((el, index) => {
info[el] = props.params[element]
? props.params[element][index]
: ''
})
delete info[element]
}
})
// 删除为空的条件
Object.keys(info).forEach((el) => {
if (!info[el]) {
delete info[el]
}
})
queryParams.value = info
}
},
{
immediate: true,
deep: true,
}
)
const handleClick = (e: any) => {
// console.log(e)
ElMessage('功能开发中...')
}
const handleExportClick = async (e: any) => {
const _info = {
offset: 0,
limit: props.total,
// sortColumn: '',
// sortOrder: '',
queryParam: {
...queryParams.value,
},
}
const _data = await AttendanceApi[e.api](_info)
const fileSetting = decodeURIComponent(_data.headers['content-disposition'])
// console.log('export---->', _data)
downLoadFile(
fileSetting.split('filename=')[1],
_data.data,
_data.headers['content-type']
)
}
</script>
<style lang="scss" scoped></style>
SearchBtn.vue
<template>
<el-form-item style="margin-left: 0px; margin-right: 0">
<div style="display: flex; height: 32px; align-items: center; width: 300px">
<el-button :icon="Search" type="primary" @click="search">查询</el-button>
<el-button :icon="Refresh" type="primary" @click="reset">重置</el-button>
<el-button
v-if="showArrow"
link
style="color: #409efc"
@click="closeSearch"
>
{{ word }}
<el-icon class="no-inherit" color="#409EFC">
<ArrowUp v-if="!showAll" />
<ArrowDown v-else />
</el-icon>
</el-button>
</div>
</el-form-item>
</template>
<script setup lang="ts">
import { Refresh, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue'
import { toRefs, computed } from 'vue'
const props = defineProps({
showAll: {
type: Boolean,
default: true,
},
showArrow: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['close-pop', 'btn-click'])
const { showAll } = toRefs(props)
const word = computed(() => {
if (showAll.value == false) {
//对文字进行处理
return '收起'
} else {
return '展开'
}
})
const closeSearch = () => {
// console.log(`子组件的状态:${showAll.value}`)
emit('close-pop')
}
const search = () => {
emit('btn-click', 'search')
}
const reset = () => {
emit('btn-click', 'reset')
}
</script>
<style lang="scss"></style>
SearchForm.vue
<template>
<el-form
ref="form"
:inline="true"
:label-width="labelWidth"
:model="condForm"
>
<div ref="formItemRef" class="btn-row">
<template v-for="(item, index) in searchFromConfig" :key="item.key">
<el-form-item
v-show="isShow(index, item)"
class="formList"
:label="item.label"
:prop="item.key"
style="margin-right: 16px"
>
<el-input
v-if="item.searchType === 'input'"
v-model="
condForm[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
clearable
:placeholder="`请输入${item.label}`"
:style="{ width: item.valueWidth || valueWidth }"
@change="conditionChange"
/>
<el-input
v-if="item.searchType === 'input-select'"
v-model="
condForm[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
clearable
placeholder="输入关键字"
:style="{ width: item.valueWidth || valueWidth }"
@change="conditionChange"
>
<template #prepend>
<el-select
v-model="
inputSelectData[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
placeholder="请选择"
style="width: 120px"
value-key="searchKey"
@change="
(e) => {
inputSelectDataChange(
e,
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
)
}
"
>
<el-option
:label="e.label"
:value="e"
v-for="(e, i) in item.searchOptions"
:key="e.searchKey"
/>
</el-select>
</template>
</el-input>
<el-select
v-if="
item.searchType === 'select' ||
item.searchType === 'select-multiple'
"
v-model="
condForm[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
clearable
:multiple="item.searchType === 'select-multiple'"
:placeholder="`请选择${item.label}`"
:style="{ width: item.valueWidth || valueWidth }"
@change="conditionChange"
>
<el-option
v-for="opt in item.dict"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-tree-select
v-if="
item.searchType === 'tree' || item.searchType === 'tree-strictly'
"
v-model="
condForm[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
check-on-click-node
:check-strictly="item.searchType === 'tree-strictly'"
collapse-tags
collapse-tags-tooltip
:data="item.dict"
:max-collapse-tags="2"
multiple
:render-after-expand="false"
show-checkbox
:style="{ width: item.valueWidth || valueWidth }"
@change="conditionChange"
/>
<el-date-picker
v-if="dateType.includes(item.searchType || '')"
v-model="
condForm[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
]
"
clearable
end-placeholder="结束时间"
:placeholder="`请选择${item.label}`"
start-placeholder="开始时间"
:style="{ width: item.valueWidth || valueWidth }"
:type="item.searchType"
:value-format="item.searchFormatDate"
@change="conditionChange"
/>
</el-form-item>
</template>
<SearchBtn
:show-all="showAll"
:show-arrow="showArrow"
@btn-click="optionClick"
@close-pop="closePop"
/>
</div>
</el-form>
</template>
<script lang="ts" setup>
import SearchBtn from './SearchBtn.vue'
import { useRoute } from 'vue-router' //1.先在需要跳转的页面引入useRouter
const router = useRoute()
const props = defineProps({
labelWidth: {
type: String,
default: '100px',
},
valueWidth: {
type: String,
default: '250px',
},
searchFromConfig: {
type: Array<any>,
default: () => [],
},
// 默认查询参数
queryParam: {
type: Object,
default: () => {},
},
})
const inputSelectData = ref(
(() => {
let obj = {}
props.searchFromConfig.forEach((item) => {
if (item.searchType === 'input-select') {
obj[
Array.isArray(item.searchKey)
? item.searchKey.join(',')
: item.searchKey || item.key
] = item.searchOptions[0]
}
})
return obj
})()
)
const inputSelectDataChange = (e: any, key: any) => {
condForm.value[key] = ''
conditionChange()
}
// 构建条件查询form,没有申明searchKey,使用key作为表单属性
const condForm = ref({})
const defaultTime: [Date, Date] = [
new Date(2000, 1, 1, 0, 0, 0),
new Date(2000, 2, 1, 23, 59, 59),
] // '12:00:00', '08:00:00'
const dateType = [
'year',
'month',
'date',
'datetime',
'week',
'datetimerange',
'daterange',
'dates',
'monthrange',
]
const extendIndex = ref<number>(20)
const form = ref<HTMLElement>()
const formItemRef = ref<HTMLElement>()
const showAll = ref(true)
const conditionNum = ref(0)
const emit = defineEmits(['condition-change'])
const typeEum: any = ['selection', 'index', 'expand', 'operation']
// 查询条件form
watch(
() => props.searchFromConfig,
async (newVal) => {
const obj = {}
conditionNum.value = props.searchFromConfig.length
newVal.forEach((el) => {
if (typeof el.searchKey === 'string') {
obj[el.searchKey] = el.searchDefaultValue || ''
} else if (Array.isArray(el.searchKey)) {
obj[el.searchKey.join(',')] = el.searchDefaultValue || ''
} else {
obj[el.key] = el.searchDefaultValue || ''
}
})
condForm.value = obj
await nextTick()
conditionChange()
const parent = formItemRef.value?.clientWidth || 0
const child =
Number(props.valueWidth.split('px')[0]) +
Number(props.labelWidth.split('px')[0]) || 350
const num = Math.floor(parent / Number(child))
if (num > conditionNum.value) {
showArrow.value = false
extendIndex.value = 20
} else {
extendIndex.value = num - 1
}
},
{
deep: true,
}
)
const showArrow = ref(true)
onMounted(async () => {
const obj = JSON.parse(JSON.stringify(router.query))
Object.keys(router.query).forEach((el) => {
const item: any = router?.query[el]
if (el.includes(',')) {
obj[el] = item.split(',')
} else {
obj[el] = item
}
})
condForm.value = obj
conditionChange()
await nextTick()
const parent = formItemRef.value?.clientWidth || 0
const child =
Number(props.valueWidth.split('px')[0]) +
Number(props.labelWidth.split('px')[0]) || 350
const num = Math.floor(parent / Number(child))
if (num > conditionNum.value) {
showArrow.value = false
extendIndex.value = 20
} else {
extendIndex.value = num - 1
}
})
const closePop = () => {
showAll.value = !showAll.value
extendIndex.value = 0 - extendIndex.value
}
const conditionChange = () => {
let keys = Object.keys(inputSelectData.value)
let _condForm = JSON.parse(JSON.stringify(condForm.value))
let _keys = Object.keys(_condForm)
// console.log(keys, _keys, _condForm)
if (keys.length > 0) {
// 存在input-select
_keys.forEach((el) => {
if (keys.includes(el)) {
_condForm[inputSelectData.value[el].searchKey] = _condForm[el]
delete _condForm[el]
}
})
}
emit('condition-change', _condForm)
}
const optionClick = (e: string) => {
const obj = {}
switch (e) {
case 'search':
// 查询
conditionChange()
break
case 'reset':
// 重置
props.searchFromConfig.forEach((el) => {
if (typeof el.searchKey === 'string') {
obj[el.searchKey] = el.searchDefaultValue || ''
} else if (Array.isArray(el.searchKey)) {
obj[el.searchKey.join(',')] = el.searchDefaultValue || ''
} else {
obj[el.key] = el.searchDefaultValue || ''
}
})
condForm.value = obj
conditionChange()
break
default:
break
}
}
const isShow = (index: any, item: any) => {
if (extendIndex.value < 0) {
return true
} else {
return index < extendIndex.value
}
}
defineExpose({
optionClick,
})
</script>
<style scoped lang="scss"></style>
TableColumn.vue
<!-- 表格列组件 -->
<template>
<el-table-column
align="center"
:fixed="column.fixed"
:label="column.label"
:prop="column.key"
:show-overflow-tooltip="true"
:sortable="column.sortable"
:width="column.width"
>
<template #default="scope">
<slot :name="column.key" :scope="scope.row">
<span v-if="!column.children">
<!-- 具有优先级 format > unit > 其他 -->
<el-tag
v-if="column.type === 'tag'"
:type="setTagType(scope.row[column.key], column.dict)"
>
<span v-if="column.format">
{{ column.format(scope.row, column.dict) }}{{ column.unit || '' }}
</span>
<span v-else>
{{ setDictValue(scope.row[column.key], column.dict) }}
{{ column.unit || '' }}
</span>
</el-tag>
<el-link
v-else-if="column.type === 'link'"
type="primary"
@click="column.linkClick(scope.row, params)"
>
<span v-if="column.format">
{{ column.format(scope.row) }}{{ column.unit || '' }}
</span>
<span v-else>{{ scope.row[column.key] }}{{ column.unit || '' }}</span>
</el-link>
<span v-else>
<span v-if="column.format">
{{ column.format(scope.row) }}{{ column.unit || '' }}
</span>
<span v-else>{{ scope.row[column.key] }}{{ column.unit || '' }}</span>
</span>
</span>
<template v-else>
<TableColumn
v-for="(child, index) in column.children"
:key="index"
:column="child"
/>
</template>
</slot>
</template>
</el-table-column>
</template>
<script lang="ts" setup>
const props = defineProps({
column: {
type: Object,
require: true,
default: () => {},
},
params: {
type: Object,
default: () => {},
},
})
const setDictValue = (value: any, opts: any) => {
const idx = opts.findIndex((el: any) => value === el.key)
if (idx !== -1) {
return opts[idx]?.name || '--'
} else {
return value || '--'
}
}
const setTagType = (value: any, opts: any) => {
const idx = opts.findIndex((el: any) => value === el.value)
if (idx !== -1) {
return opts[idx]?.type
} else {
return 'info'
}
}
</script>
<style lang="scss">
:deep(table tr span.el-tooltip__trigger) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
TablePagination.vue
<template>
<div class="customer-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="pageSizes"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</template>
vue
<script setup lang="ts">
const emit = defineEmits(['pageChange'])
const props = defineProps({
total: {
type: Number,
default: 0,
},
resetPage: {
type: Boolean,
default: false,
},
})
// 分页相关
const currentPage = ref(1)
const pageSize = ref(10)
const pageSizes = [2, 5, 10, 20, 30, 40, 50]
const handleSizeChange = (val: number) => {
pageSize.value = val
// TODO 查询
// getRecordList()
emit('pageChange', {
currentPage: currentPage.value,
pageSize: pageSize.value,
})
}
const handleCurrentChange = (val: number) => {
currentPage.value = val
// TODO 查询
// getRecordList()
emit('pageChange', {
currentPage: currentPage.value,
pageSize: pageSize.value,
})
}
/**
* 页码重置
*/
const reset = () => {
currentPage.value = 1
pageSize.value = 10
}
const returnPage = () => {
return { currentPage: currentPage.value, pageSize: pageSize.value }
}
defineExpose({
returnPage,
reset,
currentPage,
pageSize,
})
</script>
<style lang="scss"></style>
ToolBtn.vue
<template>
<div style="display: flex; margin-bottom: 8px">
<el-button
v-if="types.includes('refresh')"
circle
:icon="Refresh"
@click="handleClick"
/>
<el-button
v-if="types.includes('printer')"
circle
:icon="Printer"
@click="handleClick"
/>
<el-button
v-if="types.includes('operation')"
circle
:icon="Operation"
@click="handleClick"
/>
<el-button
v-if="types.includes('search')"
circle
:icon="Search"
@click="handleClick"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, PropType, watch, computed } from 'vue'
import { Printer, Refresh, Operation, Search } from '@element-plus/icons-vue'
const props = defineProps({
config: {
type: Array as PropType<ToolBtnCfg[]>,
default: () => [],
},
params: {
type: Object,
default: () => {},
},
})
const types = computed(() => {
return props.config.map((el) => el.type)
})
const handleClick = () => {
ElMessage('功能开发中...')
}
</script>
<style lang="scss" scoped></style>
组件使用实例
<template>
<div class="corporation-doc">
<CustomTable
api="KnowledgeBase.getKnowledgeBaseList"
:table-config="tableConfig"
:query-param="queryParam"
>
<template #opt-btn>
<el-button
type="primary"
@click="add"
:icon="Plus"
v-if="aclStore.buttonPermission.CompanyDocumentsSystemAdd"
>
添加
</el-button>
</template>
<template #rightsName="{ scope }">
<span>
<el-tag v-for="item in scope.rightsName.split(',')" :key="item">
{{ item }}
</el-tag>
</span>
</template>
</CustomTable>
</div>
</template>
<script lang="ts" setup>
import { Plus } from '@element-plus/icons-vue'
import CustomTable from '@/components/CustomTable/index.vue'
import KnowledgeBase from '@/api/knowledgeBase'
import router from '~/src/router'
import { useUserStore } from '@/store/modules/user'
import { useAclStore } from '@/store/modules/acl'
const userStore = useUserStore()
const aclStore = useAclStore()
const queryParam = ref({
type$VEQ: 'corporation',
})
const tableConfig = ref<ColumnProps[]>([
{
key: 'keywords',
label: '',
onlySearch: true,
searchType: 'input-select',
valueWidth: '400px',
searchOptions: [
{
label: '文档名称',
searchKey: 'name$VLK',
},
{
label: '文档内容',
searchKey: 'content$VLK',
},
{
label: '附件名称',
searchKey: 'attachments$VLK',
},
],
},
{
key: 'name',
label: '文档名称',
onlyTable: true,
},
{
key: 'creator',
label: '创建人',
onlyTable: true,
width: 100,
},
{
key: 'attachmentsCount',
label: '附件数',
onlyTable: true,
width: 80,
},
{
key: 'createTime',
label: '创建时间',
onlyTable: true,
width: 200,
},
{
key: 'rightsName',
label: '查看权限',
onlyTable: true,
},
{
key: 'operation',
label: '操作',
type: 'operation',
onlyTable: true,
width: 240,
btnConfig: [
{
type: 'download',
key: 'attachments',
permission: 'CompanyDocumentsSystemDownload',
showFn: (scope: any) => {
return scope.attachmentsCount > 0
},
},
{
type: 'detail',
key: 'id',
path: '/cms/document/knowledgeBaseDetail?type=corporation',
permission: 'CompanyDocumentsSystemDetail',
},
{
type: 'edit',
key: 'id',
path: '/cms/document/knowledgeBaseEdit',
permission: 'CompanyDocumentsSystemEdit',
},
{
type: 'delete',
key: 'id',
api: 'KnowledgeBase.deleteKnowledgeBase',
permission: 'CompanyDocumentsSystemDelete',
},
],
},
])
const add = () => {
router.push({
path: '/cms/document/knowledgeBaseEdit',
})
}
</script>
<style scoped lang="scss">
.corporation-doc {
padding: 20px;
}
</style>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了