feat:新增 table 组件

This commit is contained in:
JaguarJack 2024-04-23 13:11:49 +08:00
parent 3c4ebb86e7
commit eeb6fd4f41
9 changed files with 1060 additions and 0 deletions

View File

@ -0,0 +1,34 @@
<template>
<div v-if="notNeedPopover()">
{{ content }}
</div>
<el-popover v-else effect="dark" placement="top-start" :width="300" trigger="hover" :content="content">
<template #reference>
{{ ellipsis(length) }}
</template>
</el-popover>
</template>
<script setup lang="ts">
interface Prop {
content?: string
length: number
}
const props = defineProps<Prop>()
// need popover
const notNeedPopover = () => {
const length = props.content ? props.content.length : 0
return length <= 20
}
//
const ellipsis = (length: number): string => {
if (props.content) {
return props.content.substring(0, length) + '...'
}
return ''
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<el-switch @change="enabeldField" :active-value="activeValue" :inactive-value="inactiveValue" :model-value="modelValue" :loading="loading" />
</template>
<script lang="ts" setup>
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { Status } from '/admin/enum/app'
import { ref, inject } from 'vue'
const props = defineProps({
api: {
required: true,
type: String
},
id: {
required: false,
type: [String, Number]
},
field: {
require: false,
type: String,
default: 'status'
},
refresh: {
type: Function,
defaulat: null,
required: false
}
})
const modelValue = defineModel()
// @ts-ignore
const { enabled, success, loading, afterEnabled } = useEnabled()
const activeValue = ref<boolean | number | string>()
const inactiveValue = ref<boolean | number | string>()
if (typeof modelValue.value === 'boolean') {
activeValue.value = true
inactiveValue.value = false
} else {
activeValue.value = Status.ENABLE
inactiveValue.value = Status.DISABLE
}
success(() => {
modelValue.value = modelValue.value === activeValue.value ? inactiveValue.value : activeValue.value
})
afterEnabled.value = () => {
if (props.refresh) {
props.refresh()
} else {
const refresh = inject('refresh') as Function
refresh()
}
}
const enabeldField = () => {
enabled(props.api, props.id as string | number, { field: props.field })
}
</script>

View File

@ -0,0 +1,115 @@
<template>
<div class="grid w-full grid-cols-1 gap-4 pl-2 sm:grid-cols-3 lg:grid-cols-6 place-content-between">
<template v-for="(item, k) in fields" :key="k">
<div v-if="item.show && (k < 3 || expand)" class="align-items-center justify-items-center">
<el-form-item :label="item.label" v-if="item.type === 'input'">
<el-input :placeholder="item.placeholder" v-model="query[item.name]" clearable />
</el-form-item>
<el-form-item :label="item.label" v-if="item.type === 'select'">
<Select :api="item.api" :options="item.options" v-model="query[item.name]" :placeholder="item.placeholder" />
</el-form-item>
<el-form-item :label="item.label" v-if="item.type === 'input-number'">
<el-input-number v-model="query[item.name]" :placeholder="item.placeholder" clearable />
</el-form-item>
<el-form-item :label="item.label" v-if="item.type === 'date'">
<el-date-picker type="date" v-model="query[item.name]" :placeholder="item.placeholder" clearable value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item :label="item.label" v-if="item.type === 'datetime'">
<el-date-picker type="datetime" v-model="query[item.name]" :placeholder="item.placeholder" clearable value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item :label="item.label" v-if="item.type === 'tree'">
<el-tree-select v-model="query[item.name]" value-key="id" :placeholder="item.placeholder" clearable :data="item.options" check-strictly :props="item.props" />
</el-form-item>
<template v-if="item.type === 'range'">
<div class="flex flex-wrap">
<el-form-item :label="item.label">
<div class="flex flex-wrap gap-x-2">
<div v-for="(sitem, key) in item.children" :key="key">
<el-input v-model="query[item.name]" :placeholder="item.placeholder" v-if="sitem.type === 'input'" clearable />
<Select :api="item.api" :options="item.options" v-model="query[item.name]" :placeholder="item.placeholder" clearable v-if="item.type === 'select'" />
<el-input-number v-model="query[item.name]" :placeholder="item.placeholder" v-if="sitem.type === 'input-number'" clearable />
<el-date-picker type="date" v-model="query[item.name]" :placeholder="item.placeholder" v-if="sitem.type === 'date'" clearable value-format="YYYY-MM-DD" />
<el-date-picker type="datetime" v-model="query[item.name]" :placeholder="item.placeholder" v-if="sitem.type === 'datetime'" clearable value-format="YYYY-MM-DD HH:mm:ss" />
</div>
</div>
</el-form-item>
</div>
</template>
</div>
</template>
<div class="flex justify-end col-end-0 sm:col-end-5 lg:col-end-8">
<el-button type="primary" @click="search"> {{ t('system.search') }} </el-button>
<el-button @click="reset"> {{ t('system.reset') }} </el-button>
<el-button @click="expandSearch()" v-if="fields.length > 3" text circle>
<Icon :name="expand ? 'chevron-up' : 'chevron-down'" className="w-4 h-4" />
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
// @ts-nocheck
import { reactive, ref } from 'vue'
import { isBoolean, t } from '/admin/support/helper'
type itemType = 'input' | 'select' | 'input-number' | 'date' | 'datetime' | 'range'
interface Option {
label: string
value: string | number
}
interface field {
type: itemType
label: string
name: string
api?: string
placeholder?: string
default?: any
options?: Array<Option>
children?: Array<field>
show?: boolean
props?: Object // props
}
const props = defineProps({
fields: {
type: Array<field>,
default: () => {
return []
}
}
})
const emits = defineEmits(['search', 'reset'])
const query = ref<Object>({})
const expand = ref<boolean>(false)
const _fields = ref<Array<field>>(props.fields)
_fields.value = _fields.value?.map(field => {
if (!field.placeholder) {
field.placeholder = (field.type === 'select' ? '请选择' : '请输入') + field.label
}
field.show = isBoolean(field.show) ? field.show : true
return reactive(field)
})
const search = () => {
emits('search', query.value, true)
}
const reset = () => {
query.value = {}
emits('reset')
}
const expandSearch = () => {
expand.value = !expand.value
}
defineExpose({ reset })
</script>
<style lang="scss" scoped>
:deep(.el-form-item__label) {
font-size: 14px !important;
}
:deep(.el-form-item) {
margin-bottom: 0px !important;
}
</style>

View File

@ -0,0 +1,63 @@
export type columnType = 'expand' | 'selection' | 'index' | 'operate'
export type fixed = 'fiexed' | 'right' | 'left'
export interface Column {
type?: columnType // 类型 expand select index
label?: string
prop?: string
'min-width'?: string | number
width?: number | string
slot?: 'string'
header: 'string' // 表头插槽名称
align?: string
fixed?: fixed
sortable?: boolean | string
'sort-method'?: Function
'sort-by'?: Function
resizable?: boolean
formatter?: Function // function(row, column, cellValue, index)
'header-align'?: string
'class-name'?: string
selectable?: Function // function(row, index)
show: boolean
index?: number | Function // 如果设置了 type=index可以通过传递 index 属性来自定义索引
children?: Array<Column> // 多级表头
filter?:Function,
ellipsis?:boolean|number, // 当文字太多时,可以使用省略文字
switch?: boolean, // swith 字段状态切换
switchRefresh?: Function, // switch refresh 刷新
// 图片预览
image?: boolean,
preview: boolean, // 默认不预览
// 标签
tags?: boolean|Array<number>,
// 链接🔗
link?:boolean,
link_text?: string,
// 操作
update?: boolean, // 编辑操作
destroy?: boolean // 删除操作
}
// 分页
export interface paginate {
layout:string,
limit: number,
page: number,
limits: Array<number>,
total: number,
changePage: Function,
changeLimit: Function
}
// option
export interface Option {
label: string,
value: string|number
}
// 搜索 item
export interface SItem {
type: string,
name: string,
default: any,
options: Array<Option>
}

View File

@ -0,0 +1,48 @@
import { defineComponent } from 'vue'
import { ElTableColumn } from 'element-plus'
import { Column } from './ctable'
export default defineComponent({
name: 'DynamicTable',
props: {
columns: {
type: Array<Column>,
default: []
},
api: {
type: String,
default: ''
}
},
setup(props, { slots }) {
const renderColumns = (columns: Array<Column>) => {
return columns.map(column => {
if (column.children) {
return (
<ElTableColumn label="{column.label}" key="{column.label}">
{renderColumns(column.children)}
</ElTableColumn>
)
} else {
return (
<ElTableColumn label="{column.label}" prop="{column.prop}" key="{column.prop}">
{{
default: (row: any) => {
// 使用默认插槽
return row[column.prop as string]
},
customSlot: (row: any) => {
// 使用具名插槽 customSlot
return slots.customSlot ? slots.customSlot({ column, row }) : null
}
}}
</ElTableColumn>
)
}
})
}
return renderColumns(props.columns)
// return () => <ElTable data="{props.data}">{renderColumns(props.columns)}</ElTable>
}
})

View File

@ -0,0 +1,475 @@
<!--
- 数据表格
- 二次封装 ElementPlus 表格
- author JaguarJack
- 仅限 pro 试用
-->
<template>
<div>
<div v-if="searchFields.length > 0 && showSearch" class="flex flex-wrap gap-5 pt-5 pb-5 pl-5 pr-5 bg-white dark:bg-regal-dark">
<csearch :fields="searchFields" @search="doSearch" @reset="reset" ref="csearchRef" />
<slot name="csearch" />
</div>
<div class="pb-5 pl-5 pr-5 mt-3 bg-white dark:bg-regal-dark" :style="{ height: height }" ref="catch-table">
<!-- 表格头部 -->
<div class="h-16 pt-5">
<!-- 多选 -->
<div v-if="multiSelectIds.length > 0" class="flex justify-between">
<div class="flex pt-2 text-sm text-slate-400"><Icon name="check" class="mr-2" /> 已选 {{ multiSelectIds.length }} 条数据</div>
<div class="flex gap-2">
<!--批量操作的插槽-->
<slot name="multiOperate" />
<el-button @click="destroy(props.api, multiSelectIds.join(','))" type="danger" plain size="small"> <Icon name="trash" className="w-4 h-4 mr-1" />删除 </el-button>
</div>
</div>
<!-- 正常头部 -->
<div class="flex flex-row justify-between" v-else>
<div>
<Add @click="openDialog" v-if="operation" />
<el-button @click="download(exportUrl ? exportUrl : api)" v-if="exports">导出</el-button>
<!-- 头部插槽 -->
<slot name="operation" />
</div>
<!---表头 tools-->
<div class="flex flex-row justify-end w-1/3 tools" v-if="showTools">
<!-- 刷新 如果没有搜索则使用默认的重置就通过 ref 调用搜索的重置 -->
<el-button text circle @click="csearchRef ? csearchRef.reset() : reset()">
<el-icon><RefreshRight /></el-icon>
</el-button>
<!-- 密度 -->
<el-button text circle>
<el-dropdown size="small" @command="setTableSize">
<el-icon><More /></el-icon>
<template #dropdown>
<el-dropdown-menu class="w-20 mt-2">
<el-dropdown-item command="large" v-if="tableSize === 'large'">宽松</el-dropdown-item>
<el-dropdown-item command="large" v-else>宽松</el-dropdown-item>
<el-dropdown-item command="default" divided>默认</el-dropdown-item>
<el-dropdown-item command="small" divided>紧凑</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button>
<!-- 栏目 -->
<el-button text circle>
<el-popover placement="bottom" :width="100" trigger="hover" title="列设置">
<template #reference>
<Icon name="squares-plus" className="w-4 h-4" />
</template>
<template #default>
<div v-for="column in tableColumns" :key="column.prop">
<el-checkbox v-model="column.show" v-if="!column.type || column.type === 'operate'">
{{ column.label }}
</el-checkbox>
</div>
</template>
</el-popover>
</el-button>
<el-button @click="showSearch = !showSearch" text circle>
<Icon name="magnifying-glass" className="w-4 h-4" />
</el-button>
</div>
</div>
</div>
<div class="mt-3" :style="{ height: 'auto' }">
<el-table
v-loading="loading"
v-bind="$attrs"
:data="tableData"
:row-key="rowKey"
ref="catchtable"
:height="height"
:size="tableSize"
:border="border"
:empty-text="emptyText"
:summary-method="summaryMethod"
:default-sort="sort"
@sort-change="customSortChange"
@filter-change="filterChange"
@selection-change="multiSelect"
class="catchtable"
>
<!-- 无数据展示 -->
<template #empty>
<div>{{ emptyText }}</div>
</template>
<!-- column 展示 -->
<template v-for="(column, index) in tableColumns" :key="index">
<!--- selection | expand -->
<el-table-column
v-if="column.show && (column.type === 'selection' || column.type === 'expand')"
:type="column.type"
:prop="column.prop"
:aligh="column.align"
:label="column.label"
:width="column.width"
:min-width="column['min-width']"
:align="column.align"
:fixed="column.fixed"
/>
<!--- type === index -->
<el-table-column
v-if="column.show && column.type === 'index'"
:type="column.type"
:prop="column.prop"
:aligh="column.align"
:label="column.label"
:width="column.width"
:min-width="column['min-width']"
:align="column.align"
:index="index"
:fixed="column.fixed"
/>
<!--- type === operate -->
<el-table-column
v-if="column.show && column.type === 'operate'"
:aligh="column.align"
:label="column.label"
:width="column.width"
:min-width="column['min-width']"
:align="column.align"
:fixed="column.fixed"
>
<template #default="scope">
<slot name="_operate" v-bind="scope" />
<Update v-if="column.update" @click="openDialog(scope.row)" />
<Destroy v-if="column.destroy" @click="destroy(api as string, scope.row[primaryName])" />
<slot name="operate" v-bind="scope" />
</template>
</el-table-column>
<!--- 多级表头 || normal -->
<tcolumns :column="column" v-if="column.show && !column.type" :api="api" @refresh="doSearch()">
<template v-for="slot in Object.keys($slots)" #[slot]="scope: Record<string, any>">
<slot :name="slot" v-bind="scope" />
</template>
</tcolumns>
</template>
</el-table>
</div>
<!--- 分页 -->
<div v-if="showPaginate()">
<el-pagination
background
:layout="layout"
:current-page="page"
:page-size="limit"
@current-change="changePage"
@size-change="changeLimit"
:total="+total"
:page-sizes="limits"
class="flex justify-end mt-5"
/>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close :width="dialogWidth" :height="dialogHeight">
<slot name="dialog" v-bind="row" />
</Dialog>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, onMounted, provide } from 'vue'
import tcolumns from './tcolumns.vue'
// import ctcolumns from './ctcolumns'
import { Column, SItem } from './ctable'
import csearch from './csearch.vue'
import { isBoolean } from '/admin/support/helper'
import useSearch from './useSearch'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { More, RefreshRight } from '@element-plus/icons-vue'
import { useExcelDownload } from '/admin/composables/curd/useExcelDownload'
// define props
const props = defineProps({
//
api: {
type: String,
default: null
},
//
height: {
type: [String, Number],
default: () => {
return 'auto'
}
},
border: {
type: Boolean,
default: true
},
// table 'large', 'default', 'small'
size: {
type: String,
default: 'default'
},
// Key Table
rowKey: {
type: [String, Function],
default: ''
},
//
emptyText: {
type: String,
default: () => {
return '暂无数据'
}
},
// function({ columns, data })
summaryMethod: {
type: Function,
default: () => {
return null
}
},
// function({ row, column, rowIndex, columnIndex })
spanMethod: {
type: Function,
default: () => {
return null
}
},
//
pagination: {
type: Boolean,
default: true
},
//
limit: {
type: Number,
default: 10
},
//
page: {
type: Number,
default: 1
},
//
limits: {
type: Array,
default: () => {
return [10, 20, 30, 40, 50]
}
},
layout: {
type: String,
default: () => {
return 'total,sizes,prev, pager,next'
}
},
//
columns: {
type: Array<Column>,
default: () => {
return []
}
},
//
sort: {
type: Object,
default: () => {
return { order: '' }
}
},
//
sortChange: {
type: Function,
defaualt: null
},
//
filterChange: {
type: Function,
defaualt: null
},
searchForm: {
type: Array<SItem>,
default: () => {
return []
}
},
operation: {
type: Boolean,
default: true
},
showTools: {
type: Boolean,
default: true
},
primaryName: {
type: String,
default: 'id'
},
//
defaultParams: {
type: Object,
default: () => {
return {}
}
},
//
destroyConfirm: {
type: String,
default: '确定删除吗'
},
//
permissionMark: {
type: String,
default: ''
},
dialogWidth: {
type: String,
default: ''
},
dialogHeight: {
type: String,
default: ''
},
exports: {
type: Boolean,
default: false
},
exportUrl: {
type: String,
default: ''
},
searchable: {
type: Boolean,
default: true
}
})
//
const showPaginate = () => {
if (props.rowKey) {
return false
}
return props.pagination
}
//
const getDefulatParams = () => {
return props.defaultParams
}
const row = ref()
// search
const csearchRef = ref()
//
const searchFields = ref<Array<any>>(props.searchForm)
// eslint-disable-next-line vue/no-dupe-keys
const { limit, page, total, changeLimit, searchParams, changePage, doSearch, reset, loading, data, getTableData } = useSearch(props.api, showPaginate(), props.limit, props.page, getDefulatParams())
// ID
const multiSelectIds = ref<Array<string | number>>([])
// table data
const tableData = computed(() => data.value?.data)
// columns
const tableColumns = ref<Array<Column>>([])
// columns
const initColumns = () => {
props.columns.forEach(c => {
//
c.show = isBoolean(c.show) ? c.show : true
//
if (c.type === 'operate') {
c.update = isBoolean(c.update) ? c.update : true
c.destroy = isBoolean(c.destroy) ? c.destroy : true
}
tableColumns.value.push(reactive(c))
})
}
// excel
const { download } = useExcelDownload()
//
const customSortChange = (column: any, prop: string, order: string) => {
if (props.sortChange) {
return props.sortChange(column, prop, order)
} else {
//
let sort = 'desc'
if (column.order === 'ascending') {
sort = 'asc'
}
searchParams.value = Object.assign(searchParams.value, { sortField: column.prop, order: sort })
//
getTableData()
}
}
// dialog
const visible = ref<boolean>(false)
const title = ref<string>()
const openDialog = (v = null, dialogTitle: string = '') => {
row.value = v
visible.value = true
// @ts-ignore
title.value = dialogTitle || (v?.id ? '更新' : '创建')
}
const closeDialog = () => {
visible.value = false
reset()
}
//
const multiSelect = (rows: Array<any>) => {
multiSelectIds.value = []
rows.forEach(item => {
multiSelectIds.value.push(item.id)
})
}
// IDs
const getMultiSelectIds = () => {
return multiSelectIds.value
}
const tableSize = ref<string>(props.size)
// table size
const setTableSize = (command: string | number | object) => {
tableSize.value = command as string
}
const showSearch = ref<boolean>(props.searchable)
//
const { destroy, deleted } = useDestroy(props.destroyConfirm)
// onMounted
onMounted(() => {
deleted(reset)
})
//
initColumns()
getTableData()
// delete
const del = (api: string, id: any) => {
destroy(api, id)
reset()
}
//
const setDefaultParams = (params: Object) => {
searchParams.value = Object.assign(searchParams.value, params)
searchParams.value = Object.assign(searchParams.value, getDefulatParams())
}
//
defineExpose({ doSearch, openDialog, del, closeDialog, reset, setDefaultParams, getMultiSelectIds })
//
provide('closeDialog', () => {
closeDialog()
})
provide('refresh', () => {
doSearch()
})
</script>
<style scoped scss>
:deep(.tools .el-button) {
background-color: var(--el-fill-color-light) !important;
}
:deep(.catchtable .el-table__header .el-table__cell) {
background: var(--catch-table-header-bg-color) !important;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<el-table-column :label="column.label" v-if="column.children">
<tcolumns :column="child" v-for="(child, k) in column.children" :key="k" :api="api">
<template v-for="slot in Object.keys($slots)" #[slot]="scope: Record<string, any>">
<slot :name="slot" v-bind="scope" />
</template>
</tcolumns>
</el-table-column>
<el-table-column
:type="column.type"
:prop="column.prop"
:aligh="column.align"
:label="column.label"
:width="column.width"
:min-width="column['min-width']"
:align="column.align"
:sortable="column.sortable"
:formatter="column.formatter"
v-else
>
<!-- 自定义 column header 插槽 -->
<template #header v-if="column.header">
{{ column.width }}
<slot :name="column.header" />
</template>
<!-- 插槽内容 -->
<template #default="scope">
<!-- 自定义 -->
<slot :name="column.slot" v-bind="scope" v-if="column.slot" />
<!-- default -->
<slot :name="column.prop" v-bind="scope" v-if="column.prop && !column.slot">
<!-- 长字段省略号显示 -->
<span v-if="column.ellipsis">
<Ellipsis :content="scope.row[column.prop]" :length="isBoolean(column.ellipsis) ? 20 : (column.ellipsis as number)" />
</span>
<!-- switch 字段切换-->
<span v-else-if="column.switch && column.prop">
<SwitchColumn :field="column.prop" :api="api" :id="scope.row.id" v-model="scope.row[column.prop]" :refresh="column.switchRefresh || refresh" />
</span>
<!-- 图片预览 -->
<span v-else-if="column.image && column.prop">
<div v-if="scope.row[column.prop]">
<el-image
width="100"
class="preview_img"
:src="getImage(column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop])"
:z-index="10000"
style="width: 50px; height: 50px"
v-if="!column.preview"
>
<template #error>
<div class="image-slot">
<el-icon><icon-picture /></el-icon>
</div>
</template>
</el-image>
<el-image
class="cursor-pointer preview_img"
:src="getImage(column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop])"
:z-index="10000"
:initial-index="0"
style="width: 50px; height: 50px"
:preview-teleported="true"
:preview-src-list="column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop]"
v-else
>
<template #error>
<div class="image-slot">
<el-icon><icon-picture /></el-icon>
</div>
</template>
</el-image>
</div>
</span>
<!-- 链接 -->
<span v-else-if="column.link && column.prop">
<a :href="column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop]" target="_blank">
<el-tag>{{ column.link_text ? column.link_text : '链接' }}</el-tag>
</a>
</span>
<!-- 标签 -->
<span v-else-if="column.tags && column.prop">
<el-tag :type="getTagsType(column.tags, scope.row[column.prop])">{{ column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop] }}</el-tag>
</span>
<!-- 普通字段 -->
<span v-else>
{{ column.filter ? column.filter(scope.row[column.prop]) : scope.row[column.prop] }}
</span>
</slot>
</template>
</el-table-column>
</template>
<script lang="ts" setup>
import { isBoolean, isUndefined } from '/admin/support/helper'
import { Column } from './ctable'
import Ellipsis from './components/ellipsis/index.vue'
import SwitchColumn from './components/switchColumn/index.vue'
import { Picture as IconPicture } from '@element-plus/icons-vue'
interface Props {
column: Column
api: string
}
defineProps<Props>()
const getImage = (src: string | Array<string>) => {
if (isUndefined(src) || src === null) {
return null
}
if (typeof src === 'string') {
return src
}
return src[0]
}
const getTagsType = (tags: Array<number> | boolean, value: number) => {
if (typeof tags === 'boolean') {
return 'primary'
}
if (value) {
const res = tags[value - 1]
return res || 'primary'
}
return 'primary'
}
// switch
const emits = defineEmits(['refresh'])
const refresh = () => {
emits('refresh')
}
</script>
<style scoped lang="scss">
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
font-size: 30px;
}
.image-slot .el-icon {
font-size: 30px;
}
</style>

View File

@ -0,0 +1,19 @@
export type itemType = 'input' | 'select' | 'input-number' | 'date' | 'datetime' | 'range'
export interface Option {
label: string
value: string | number
}
export interface Field {
type: itemType
label: string
name: string
api?: string
placeholder?: string
default?: any
options?: Array<Option>
children?: Array<Field>
show?: boolean
props?: Object // 树形 props
}

View File

@ -0,0 +1,93 @@
import { ref } from 'vue'
import http from '/admin/support/http'
interface response {
code: number
data: Array<Object> | Object
message: string
total?: number
page?: number
limit?: number
}
export default function useSearch(api: string | Function, isPagination: boolean, pageLimit: number, currentPage: number, defaultParams: Object = {}) {
// 默认设置分页参数
const limit = ref<number>(pageLimit)
const page = ref<number>(currentPage)
const total = ref<number>(0)
const loading = ref<boolean>(false)
// 搜索参数
const searchParams = ref<Object>({})
const data = ref<response>()
if (isPagination) {
searchParams.value = Object.assign(searchParams.value, { page: currentPage, limit: pageLimit })
}
// 分页切换
const changePage = (p: number) => {
page.value = p
setPaginateParams(p)
}
// limit 切换
const changeLimit = (l: number) => {
limit.value = l
page.value = 1
setPaginateParams(0, l)
}
// 设置分页查询参数
const setPaginateParams = (page: number = 0, limit: number = 0) => {
if (page) {
// @ts-ignore
searchParams.value.page = page
}
if (limit) {
// @ts-ignore
searchParams.value.limit = limit
}
getTableData()
}
// 执行 search
const doSearch = (params: Object | null = null, append: boolean = true) => {
if (params && !append) {
searchParams.value = params
} else {
searchParams.value = Object.assign(searchParams.value, params)
}
getTableData()
}
// 重置 || 刷新
const reset = () => {
searchParams.value = Object.assign({})
if (isPagination) {
// 这里会调用 getTableData 方法
setPaginateParams(currentPage, pageLimit)
} else {
getTableData()
}
}
const getTableData = () => {
loading.value = true
setDefaultParams()
http.get(api as string, searchParams.value).then(r => {
loading.value = false
data.value = r.data
if (isPagination) {
total.value = r.data.total
}
})
}
// 设置搜索默认参数
const setDefaultParams = () => {
searchParams.value = Object.assign(searchParams.value, defaultParams)
}
return { limit, page, total, changeLimit, setPaginateParams, searchParams, changePage, doSearch, reset, loading, data, getTableData }
}