gif不清楚 视频地址 (视频可见:getOne 即请求自定义配置接口只会触发一次,除非主动清楚缓存)
routerStart==> 路由开始
routerDone==> 路由结束
useCommonHooks==> 请求配置hooks(请求自定义列配置只会在此处触发一次,其余调用皆走缓存,除非主动清除缓存)
gridMount==> 表格组件渲染钩子
gridQueryRes==> 页面数据请求完毕
gridLoadColumns==> 加载列
queryHeaderOptions==> 递归请求表头下拉options
表格组件封装Grid
<Grid
ref="gridRef"
:columns="tableColumn"
:toolbar-button="toolbarButtonConfig"
:operte-button="operateButtonConfig"
:query-api="yourapi"
@click-fall-back="clickFallBack"
@query-options="queryHeaderOptions()"
/>
import { useCommonTableConfig } from '@/hooks/useCommon'
//此处会触发2 queryHeaderOptions 为递归请求表头下拉options 在表格加载列完成后会触发此方法
const { tableColumn, queryHeaderOptions } = useCommonTableConfig()
/**
* 组合生成column (JSON转换成列,根据业务需求自行实现)
*/
const patchTableColumn = (data: CustomColumnProps[]): ColumnProps[] => {
const temp: ColumnProps[] = [
{
type: 'checkbox',
width: 50,
fixed: 'left'
}
]
for (let index = 0; index < data.length; index++) {
const element = data[index]
const { slots, field, title, sortable, minWidth, width, selectField, loading, options, rules } = element
const obj: ColumnProps = {
slots: {
header: slots
},
children: [{ field, title, sortable, selectField, minWidth, width, formatter: formatterFuncMap[field] || null }]
}
if (loading !== undefined) obj.loading = loading
if (options !== undefined) {
obj.options = options
if (!options.length) {
// 需要请求表头下拉
queryHeaderField.value.push({
field,
api: queryHeaderApiMap[field]
})
}
}
if (rules !== undefined)
obj.rules = {
message: rules.message,
func: (val) =>
rules.type === 'decimal'
? validDecimal(rules.maxLength as number, rules.maxDecimal as number, val)
: validInteger(rules.maxLength as number, val)
}
temp.push(obj)
}
return temp
}
/**
* 请求表头列
*/
const queryColumns = () => {
listCustomize($route.name as string)
.then((res) => {
const { data } = res
if (data) {
const { content } = data
columns.value = [...patchTableColumn(JSON.parse(content))]
} else {
// 抛出异常 获取默认
throw new Error()
}
})
.catch(() => {
// 默认
const { jsonType } = $route.meta
if (!jsonType)
console.warn(`your warning`)
jsonType &&
listDefault(jsonType).then((res: any) => {
const { data } = res
columns.value = [...patchTableColumn(data)]
})
})
}
onBeforeMount(() => {
console.log('useCommonHooks==>', new Date().getTime())
queryColumns()
})
npm i @vueuse/core
import { useMemoize } from '@vueuse/core'
const jsonFiles = import.meta.globEager('@/json/*.json')
/**
*
* @desc 默认json示例 数组 仅示例 业务无关
*/
{
"slots": "select",
"field": "fieldDesc",
"title": "title",
"sortable": true,
"selectField": "field",
"loading": false,
"options": []
}
/**
* 获取列表自定义信息详情
* code 必须为string 设为路由name 保证唯一
*/
export const listCustomize = useMemoize(async (code: string) => {
const list = await service.get(`yourCustomizeApi`)
return list
})
export const deleteListCustomizeCache = (code: string) => listCustomize.delete(code)
export const clearListCustomizeCache = () => listCustomize.clear()
/**
*
* @desc 添加或修改列表自定义信息
*/
export const createOrUpdate = (params: object) =>
service.post(`yourApi`, params)
/**
*
* @desc 模拟请求 本地默认列
*/
const getColumnJson = (jsonType: string) => {
return new Promise((resolve) => {
const key = Object.keys(jsonFiles).filter((jsonKey) => jsonKey.includes(jsonType))[0]
resolve({
data: jsonFiles[key]['default'] || [],
status: 0
})
})
}
/**
*
* @desc 获取本地json默认列
*/
export const listDefault = useMemoize(async (jsonType: string) => {
const columns = await getColumnJson(jsonType)
return columns
})
//Grid 组件自定义工具栏 点击弹出自定义列配置弹框
<template>
<vxe-grid v-bind="gridOptions" ref="xGrid" @page-change="pageChange" @sort-change="sortChange">
<template #toolbar_tools>
<div class="vxe-tools--wrapper">
<button
v-show="customConfig"
class="vxe-button type--button size--small is--circle"
type="button"
title="列设置"
@click="customlayout"
>
<i class="vxe-button--icon vxe-icon--menu"></i>
</button>
</div>
</template>
</vxe-grid>
<n-modal
v-model:show="showModal"
preset="card"
:title="`${t(`route.${$route.meta.title}`)}列表自定义`"
:mask-closable="false"
style="width: 980px; margin-top: 150px"
:z-index="999"
>
<custom-layout @hide-modal="hideModal" />
</n-modal>
</template>
npm i sortablejs @types/sortablejs
//customLayout.tsx
import {
NSpace,
NButton,
NTooltip,
NSpin,
NFormItem,
NInputNumber,
type FormItemRule,
type FormItemInst
} from 'naive-ui'
import type { VxeGridProps, VxeTableInstance } from 'vxe-table'
import { createOrUpdate, listCustomize, deleteListCustomizeCache, listDefault } from '@/api/modules/app'
type Sortable = import('sortablejs')
import { type SortableEvent } from 'sortablejs'
type GridDataProps = {
title: string
field: string
}
export default defineComponent({
name: 'CustomLayout',
emits: {
hideModal: (refresh?: boolean) => true
},
setup(props, { emit }) {
const $route = useRoute()
const loading = ref(false)
const sortable = shallowRef<Sortable | null>(null)
const leftGrid = ref({} as VxeTableInstance)
const rightGrid = ref({} as VxeTableInstance)
/**
* 默认column json
*/
const leftGridOptions = reactive<VxeGridProps>({
height: 500,
size: 'small',
rowConfig: {
isCurrent: true,
isHover: true
},
columns: [
{
title: '全部字段列表',
headerClassName: 'text-blue',
children: [
{
type: 'checkbox',
width: 50
},
{
type: 'seq',
width: 50
},
{
title: '字段名',
field: 'title',
minWidth: 150
}
]
}
],
checkboxConfig: {
checkMethod: ({ row }) => !selectFields.value!.includes(row.title)
},
data: []
})
const rightGridOptions = reactive<VxeGridProps>({
height: 500,
size: 'small',
rowConfig: {
isCurrent: true,
isHover: true,
useKey: true
},
editConfig: {
trigger: 'click',
mode: 'cell'
},
emptyText: '当前页面未配置自定义列表!',
columns: [
{
title: '展示字段列表',
headerClassName: 'text-blue',
children: [
{
type: 'checkbox',
width: 50
},
{
type: 'seq',
width: 50
},
{
title: '字段名',
field: 'title',
minWidth: 150
},
{
title: '列宽',
titlePrefix: {
content: '正整数,50-500'
},
field: 'width',
width: 100,
editRender: {
name: '$input',
immediate: true,
props: {
type: 'number',
placeholder: '请输入列宽',
clearable: false,
min: 50,
max: 500
},
defaultValue: 50,
placeholder: '请输入列宽'
}
},
{
title: '排序方式',
field: 'sortType',
width: 120,
titlePrefix: {
content: '升、降序只能生效一个字段'
},
editRender: {
name: '$select',
immediate: true,
options: [
{
label: '不排序',
value: false
},
{
label: '默认升序',
value: 'asc'
},
{
label: '默认降序',
value: 'desc'
}
],
events: {
change: ({ row }, { value }) => sortModeChange(row, value)
},
props: {
transfer: true,
placeholder: '请选择排序方式',
clearable: false
},
placeholder: '请选择排序方式'
}
},
{
title: '操作',
width: 80,
slots: {
default: 'operateCell'
}
},
{
width: 50,
slots: {
default: 'dragSlot',
header: 'dragSlotHeader'
}
}
]
}
],
data: []
})
/**
* 已展示的字段
*/
const selectFields = computed(() => rightGridOptions.data?.map((row) => row.title))
/**
*T排序方式设定
设置为非 false 时,升序、降序仅能留存一个,设置其他为false
*/
const sortModeChange = (currentRow, value) => {
if (value) {
const otherRowHasSort = rightGridOptions.data
?.filter((row) => row.sortType)
.filter((_row) => _row.title !== currentRow.title)
if (otherRowHasSort?.length) {
otherRowHasSort.forEach((otherRow) => {
const otherRowIndex = rightGrid.value.getRowIndex(otherRow)
rightGridOptions.data!.at(otherRowIndex).sortType = false
})
}
}
}
/**
* 展示 隐藏字段
*/
const handleClick = (cancel = false) => {
if (cancel) {
// 取消
const selectRow = rightGrid.value?.getCheckboxRecords()
if (!selectRow.length) return window.$message.warning('请选取至少一条展示字段!')
rightGrid.value.removeCheckboxRow().then(({ rows }) => {
rows.forEach((row) => {
const tempIndex = rightGridOptions.data?.findIndex((ele) => ele.title === row.title)
rightGridOptions.data?.splice(tempIndex as number, 1)
})
})
} else {
const selectRow = leftGrid.value?.getCheckboxRecords()
if (!selectRow.length) return window.$message.warning('请选取至少一条列表字段!')
rightGridOptions.data?.push(
...selectRow.map((row) => {
return {
...row,
width: row.minWidth || 200,
sortType: false
}
})
)
rightGrid.value.reloadData(rightGridOptions.data as [])
leftGrid.value.clearCheckboxRow()
}
}
/**
* 保存
* @param cancel 是否取消
* @returns
*/
const formSubmit = (cancel = false) => {
if (cancel) return emit('hideModal')
if (!rightGridOptions.data?.length) return window.$message.warning('请配置至少一条展示字段!')
loading.value = true
createOrUpdate({
resourceCode: $route.name,
content: JSON.stringify(rightGridOptions.data)
})
.then((res) => {
const { msg } = res
window.$message.success(msg || '操作成功!')
deleteListCustomizeCache($route.name as string) //清除缓存
emit('hideModal', true)
})
.finally(() => {
loading.value = false
})
}
/**
* 获取默认
*/
const getListDefault = async () => {
leftGridOptions.loading = true
listDefault($route.meta.jsonType)
.then((res: any) => {
const { data } = res
leftGridOptions.data = [].concat(data)
})
.finally(() => {
leftGridOptions.loading = false
})
}
/**
* 获取已保存
*/
const getListCustomize = async () => {
rightGridOptions.loading = true
listCustomize($route.name as string)
.then((res) => {
const { data } = res
if (data) {
const { content } = data
rightGridOptions.data = [...JSON.parse(content)]
}
})
.finally(() => {
rightGridOptions.loading = false
})
}
/**
* 置顶 |移至指定行
*/
const handleMoveRow = (toTop: boolean, rowIndex: number) => {
if (toTop) {
// 置顶
const currentRow = rightGridOptions.data?.splice(rowIndex, 1)[0]
rightGridOptions.data?.splice(0, 0, currentRow)
rightGrid.value.reloadData(rightGridOptions.data as [])
} else {
const seq = ref<number | null>(rowIndex + 1)
const seqRef = ref<FormItemInst | null>(null)
const rule: FormItemRule = {
required: true,
trigger: ['input', 'blur'],
validator() {
if (seq.value === null) return new Error('请输入行序号!')
if (seq.value === rowIndex + 1) return new Error('请输入不同的行序号!')
if (!/^\d*$/.test(seq.value + '')) {
return new Error('行序号应该为大于1的整数!')
}
}
}
window.$useDialog.info({
title: '移至指定行',
showIcon: false,
style: {
width: '500px'
},
content: () =>
h(
'div',
{
class: 'text-base'
},
h(
NFormItem,
{
ref: seqRef,
label: '行序号',
labelPlacement: 'left',
labelWidth: 120,
rule
},
{
default: () =>
h(NInputNumber, {
class: 'w-full text-left',
placeholder: '请输入行序号',
buttonPlacement: 'both',
min: 1,
max: rightGridOptions.data?.length,
value: seq.value,
autofocus: true,
'onUpdate:value': (value: number | null) => (seq.value = value)
}),
label: () =>
h('span', [
'行序号',
h(
NTooltip,
{
trigger: 'hover'
},
{
trigger: () => (
<iconpark-icon
name="info"
color="currentColor"
size="16"
class="cursor-help vertical-middle op-80"
></iconpark-icon>
),
default: () => '整数,1-展示总条数'
}
)
])
}
)
),
positiveText: '确定',
negativeText: '取消',
iconPlacement: 'top',
maskClosable: false,
onPositiveClick() {
return new Promise((resolve, reject) => {
seqRef.value?.validate({
callback: (errors) => {
if (errors) {
reject()
} else {
const currentRow = rightGridOptions.data?.splice(rowIndex, 1)[0]
rightGridOptions.data?.splice((seq.value as number) - 1, 0, currentRow)
rightGrid.value.reloadData(rightGridOptions.data as [])
resolve(null)
}
}
})
})
},
onNegativeClick: async () => {
return true
}
})
}
}
/**
* 初始化 sortablejs 行拖动
*/
const initSortable = () => {
const { value } = rightGrid
sortable.value = Sortable.create(value.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
handle: '.drag',
onEnd: (sortableEvent: SortableEvent) => {
const { oldIndex, newIndex } = sortableEvent
const currentRow = rightGridOptions.data?.splice(oldIndex as number, 1)[0]
rightGridOptions.data?.splice(newIndex as number, 0, currentRow)
rightGrid.value.reloadData(rightGridOptions.data as [])
}
})
}
onMounted(() => {
getListDefault()
getListCustomize()
initSortable()
})
onBeforeUnmount(() => {
sortable.value?.destroy()
})
return {
leftGridOptions,
rightGridOptions,
leftGrid,
rightGrid,
handleClick,
formSubmit,
handleMoveRow,
loading
}
},
render() {
const { leftGridOptions, rightGridOptions, loading } = this
return (
<div>
<NSpin stroke="#409eff" show={loading}>
<NSpace vertical size={10}>
<NSpace justify="space-between" align="center" wrap={false}>
<vxe-grid ref={(el) => (this.leftGrid = el)} {...leftGridOptions}></vxe-grid>
<NSpace vertical justify="center" align="center" class="h-full" style="--iconColor: #606266">
<NTooltip trigger="hover">
{{
trigger: () => (
<iconpark-icon
name="to-right"
size="28"
class="cursor-pointer text-$iconColor"
onClick={() => this.handleClick()}
></iconpark-icon>
),
default: () => '展示字段'
}}
</NTooltip>
<NTooltip trigger="hover">
{{
trigger: () => (
<iconpark-icon
name="to-left"
size="28"
class="cursor-pointer text-$iconColor"
onClick={() => this.handleClick(true)}
></iconpark-icon>
),
default: () => '取消展示'
}}
</NTooltip>
</NSpace>
<vxe-grid ref={(el) => (this.rightGrid = el)} {...rightGridOptions}>
{{
dragSlot: () => (
<iconpark-icon
name="drag"
size="16"
color="#409eff"
class="cursor-move vertical-middle drag"
></iconpark-icon>
),
dragSlotHeader: () => (
<NTooltip trigger="hover">
{{
trigger: () => <i class="vxe-cell-help-icon vxe-icon--question"></i>,
default: () => '按住后可以上下拖动排序'
}}
</NTooltip>
),
operateCell: ({ rowIndex }) => (
<NSpace>
<NTooltip trigger="hover" disabled={!rowIndex}>
{{
trigger: () => (
<iconpark-icon
name="minus-the-top"
size="14"
color="currentColor"
class={[rowIndex ? 'visible cursor-pointer' : 'invisible']}
onClick={() => this.handleMoveRow(true, rowIndex)}
></iconpark-icon>
),
default: () => '置顶'
}}
</NTooltip>
<NTooltip trigger="hover">
{{
trigger: () => (
<iconpark-icon
name="bring-to-front-one"
size="14"
color="currentColor"
class="cursor-pointer"
onClick={() => this.handleMoveRow(false, rowIndex)}
></iconpark-icon>
),
default: () => '移至指定行'
}}
</NTooltip>
</NSpace>
)
}}
</vxe-grid>
</NSpace>
<NSpace justify="end" align="center" size={10}>
<NButton secondary onClick={() => this.formSubmit(true)}>
取消
</NButton>
<NButton type="info" onClick={() => this.formSubmit()}>
保存
</NButton>
</NSpace>
</NSpace>
</NSpin>
</div>
)
}
})
over~