hooks
useConfirm.ts
import { ElMessageBox, ElMessage } from "element-plus";
import { ResultEnum } from '/@/enums/httpEnum';
const useConfirm = (
api: (params: any) => Promise<any>,
params: any = {},
message: string,
confirmType: 'warning'
) => {
return new Promise((resolve, reject) => {
ElMessageBox.confirm(`${message}`, "温馨提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: confirmType,
draggable: true
}).then(async () => {
const res = await api(params);
if (!res) return reject(false);
ElMessage({
type: "success",
message: `操作成功!`
});
resolve(true);
});
});
};
export default useConfirm;
useSelection.ts
import { ref, computed } from "vue";
const useSelection = (rowKey: string = 'guid') => {
const isSelected = ref<boolean>(false);
const selectedList = ref<{ [key: string]: any }[]>([]);
const selectedListIds = computed((): string[] => {
return selectedList.value.map(item => item[rowKey]);
});
const selectListCount = computed((): number => {
return selectedList.value.length;
});
const onSelectionChange = (rowArr: { [key: string]: any }[]) => {
rowArr.length ? (isSelected.value = true) : (isSelected.value = false);
selectedList.value = rowArr;
};
return {
isSelected,
selectedList,
selectedListIds,
selectListCount,
onSelectionChange
};
};
export default useSelection;
useTable.ts
import { computed, reactive, toRefs } from "vue";
const DEFAULT_PAGE_SIZE = 10;
export namespace Table {
export interface Pagination {
pageIndex: number;
pageSize: number;
total: number;
}
export interface StateProps {
loading: Boolean;
searchParam: Object;
totalParam: Object;
tableData: any[];
pages: Pagination;
}
}
export const useTable = (
api: (params: any) => Promise<any>,
initParam: object = {},
immediate: boolean = true,
isShowPage: boolean = true,
onError?: (error: any) => void
) => {
const state = reactive<Table.StateProps>({
loading: false,
searchParam: {},
totalParam: {},
tableData: [],
pages: {
pageIndex: 1,
pageSize: DEFAULT_PAGE_SIZE,
total: 0
}
});
const pageParam = computed(() => {
return {
pageIndex: state.pages.pageIndex,
pageSize: state.pages.pageSize
};
});
const getTableList = async () => {
if (!api) return;
state.loading = true;
try {
Object.assign(state.totalParam, initParam, isShowPage ? pageParam.value : {});
const { pages, data } = await api({ ...state.totalParam });
state.tableData = data ?? [];
isShowPage && updatePages(pages);
} catch (error) {
onError && onError(error);
} finally {
state.loading = false;
}
};
const isImmediate = immediate ?? true;
onMounted(() => isImmediate && getTableList());
const updatePages = (pages: Table.Pagination) => {
Object.assign(state.pages, pages);
};
const onSearch = () => {
state.pages.pageIndex = 1;
getTableList();
};
const onReset = () => {
state.pages.pageIndex = 1;
getTableList();
};
const onRefresh = () => {
getTableList();
};
const onPageChange = (pages: TableDemoPageType) => {
state.pages.pageIndex = pages.pageIndex;
state.pages.pageSize = pages.pageSize;
getTableList();
};
return {
...toRefs(state),
getTableList,
onPageChange,
onSearch,
onReset,
onRefresh
}
};
export default useTable;
customTable封装
<template>
<div :class="[!setBorder && 'el-table-no-border', 'table-container']">
<header class="table-header mb12">
<div class="table-header-operation">
<slot name="operation-list" />
</div>
<div v-if="showTools" class="table-header-tool">
<SvgIcon
name="iconfont icon-shuaxin"
:size="22"
title="刷新"
@click="onRefreshTable"
/>
<SvgIcon
name="iconfont icon-fullscreen"
:size="20"
title="全屏"
@click="onCurrenFullscreen"
/>
<el-popover
placement="top-end"
trigger="click"
transition="el-zoom-in-top"
popper-class="table-tool-popper"
:width="300"
:persistent="false"
@show="onSetTable"
>
<template
<SvgIcon name="iconfont icon-quanjushezhi_o" :size="22" title="设置" />
</template>
<template
<div class="tool-box">
<el-tooltip content="拖动进行排序" placement="top-start">
<SvgIcon
name="fa fa-question-circle-o"
:size="16"
class="ml11"
color="#909399"
/>
</el-tooltip>
<el-checkbox
v-model="state.checkListAll"
:indeterminate="state.checkListIndeterminate"
class="ml10 mr1"
label="列显示"
@change="onCheckAllChange"
/>
<el-checkbox v-model="getConfig.isSerialNo" class="ml12 mr1" label="序号" />
<el-checkbox
v-model="getConfig.isSelection"
class="ml12 mr1"
label="多选"
/>
</div>
<el-scrollbar>
<div ref="toolSetRef" class="tool-sortable">
<div
class="tool-sortable-item"
v-for="v in columns"
:key="v.prop"
:data-key="v.prop"
>
<i class="fa fa-arrows-alt handle cursor-pointer"></i>
<el-checkbox
v-model="v.isCheck"
size="default"
class="ml12 mr8"
:label="v.label"
@change="onCheckChange"
/>
</div>
</div>
</el-scrollbar>
</template>
</el-popover>
</div>
</header>
<el-table
ref="tableRef"
v-bind="$attrs"
v-loading="loading"
border
:data="tableData"
:row-key="rowKey"
style="width: 100%"
@selection-change="onSelectionChange"
@cell-dblclick="onCellDblClick"
>
<el-table-column
type="selection"
align="center"
:reserve-selection="true"
:resizable="false"
width="60"
v-if="config.isSelection"
/>
<el-table-column
type="index"
align="center"
label="序号"
width="90"
:index="getIndex"
:resizable="false"
v-if="showSerialNo"
/>
<el-table-column
v-for="(item, index) in setHeader"
:key="index"
:align="item.align ?? 'center'"
:sortable="item.sortable"
:width="item.width"
:min-width="item.minWidth ?? 90"
:label="item.label"
:prop="item.prop"
:fixed="item.fixed"
:formatter="item.formatter ?? emptyFormatter"
:resizable="index + 1 < setHeader.length"
:reserve-selection="item.reserveSelection ?? false"
:show-overflow-tooltip="item.showOverflowTooltip ?? true"
>
<!-- 自定义行slot -->
<template v-if="item.isCustom"
<slot
:name="item.prop"
:row="scope.row"
:column="scope.column"
:index="scope.$index"
/>
</template>
<template v-if="item.children && item.children.length > 0">
<el-table-column
:key="child.prop"
v-for="child in item.children"
v-bind="child"
/>
</template>
</el-table-column>
<template
<el-empty description="暂无数据" />
</template>
</el-table>
<el-divider class="table-divider" />
<div class="table-footer">
<div class="table-footer-total-text flex-y-center">
<section class="mr24">
{{ state.page.pageSize }}项每页, 共{{ pages.total }}项
</section>
<section class="flex-y-center ml-30" v-if="isSelected">
<div class="">已选择{{ selectListCount }}项</div>
<el-divider direction="vertical" />
<el-button type="primary" text @click.stop="onClearSelection">取消</el-button>
</section>
</div>
<el-pagination
v-model:current-page="state.page.pageIndex"
v-model:page-size="state.page.pageSize"
:pager-count="5"
:page-sizes="[10, 20, 50, 100, 500]"
:total="pages.total"
layout="sizes, prev, pager, next, jumper"
background
@size-change="onHandleSizeChange"
@current-change="onHandleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts" name="nextTable">
import Sortable from 'sortablejs'
import mittBus from '/@utils/mitt'
import { useThemeConfig } from '/@/stores/themeConfig'
import { useSelection } from '/@hooks'
import '/@/theme/tableTool.scss'
// 默认分页
const DEFAULT_PAGE_SIZE = 10
// 定义父组件传过来的值
const props = defineProps({
// 列表状态
loading: { type: Boolean, default: false },
// 列表Key
rowKey: { type: String, default: 'guid' },
// 表头内容
columns: { type: Array<EmptyObjectType>, default: () => [] },
// 列表内容
tableData: { type: Array<EmptyObjectType>, default: () => [] },
// 配置项
config: { type: Object, default: () => ({}) },
// 分页入参
pages: { type: Object, default: () => ({}) },
})
// 定义子组件向父组件传值/事件
const emit = defineEmits(['delRow', 'pageChange', 'sortHeader', 'refresh'])
// 定义变量内容
const route = useRoute()
const toolSetRef = ref()
const tableRef = ref()
const storesThemeConfig = useThemeConfig()
const { themeConfig } = storeToRefs(storesThemeConfig)
const state = reactive({
page: {
pageIndex: 1,
pageSize: DEFAULT_PAGE_SIZE,
},
checkListAll: true,
checkListIndeterminate: false,
})
// 设置边框显示/隐藏
const setBorder = computed(() => !!props.config?.isBorder ?? false)
// 获取父组件 配置项(必传)
const getConfig = computed(() => props.config ?? {})
// 设置 tool header 数据
const setHeader = computed(() => props.columns.filter((v) => v.isCheck))
// 是否显示序号,默认显示
const showSerialNo = computed(() => props?.config?.isSerialNo ?? true)
// 是否显示工具栏,默认显示
const showTools = computed(() => props?.config?.isShowTools ?? true)
// 表格多选 Hooks
const { onSelectionChange, selectedList, selectListCount, isSelected } = useSelection(props.rowKey)
// 清空选中数据列表
const onClearSelection = () => tableRef.value!.clearSelection()
// 列表空数据处理
const emptyFormatter = (row, column) => {
const { property } = column
return row?.[property] ?? '--'
}
// tool 列显示全选改变时
const onCheckAllChange = <T>(val: T) => {
if (val) props.columns.forEach((v) => (v.isCheck = true))
else props.columns.forEach((v) => (v.isCheck = false))
state.checkListIndeterminate = false
}
// tool 列显示当前项改变时
const onCheckChange = () => {
const headers = props.columns.filter((v) => v.isCheck).length
state.checkListAll = headers === props.columns.length
state.checkListIndeterminate = headers > 0 && headers < props.columns.length
}
// 双击单元格复制文本
const onCellDblClick = (row: any, column: any, cell: HTMLTableCellElement, event: Event) => {
const target = event.target
if (target && target.innerText) {
// 双击选中文本并拷贝到剪切板
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(event.target)
selection.removeAllRanges()
selection.addRange(range)
document.execCommand('Copy')
ElMessage.success('复制成功!')
}
}
// 删除当前项
const onDelRow = (row: EmptyObjectType) => {
emit('delRow', row)
}
// 分页改变
const onHandleSizeChange = (size: number) => {
state.page.pageSize = size
emit('pageChange', state.page)
}
// 分页改变
const onHandleCurrentChange = (page: number) => {
state.page.pageIndex = page
emit('pageChange', state.page)
}
// 序号
const getIndex = (index: number) => {
const { pageIndex, pageSize } = state.page
return ((pageIndex ?? 1) - 1) * (pageSize ?? DEFAULT_PAGE_SIZE) + index + 1
}
// 全屏
const onCurrenFullscreen = () => {
mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 4, ...route }))
}
// 刷新
const onRefreshTable = () => {
emit('refresh', state.page)
}
// 设置
const onSetTable = () => {
nextTick(() => {
const sortable = Sortable.create(toolSetRef.value, {
handle: '.handle',
dataIdAttr: 'data-key',
animation: 150,
onEnd: () => {
const headerList: EmptyObjectType[] = []
sortable.toArray().forEach((val: string) => {
props.columns.forEach((v) => {
if (v.prop === val) headerList.push({ ...v })
})
})
emit('sortHeader', headerList)
},
})
})
}
// 分页监听
watch(
props.pages,
() => {
const { pageIndex, pageSize } = props.pages ?? {}
state.page.pageIndex = pageIndex ?? 1
state.page.pageSize = pageSize ?? DEFAULT_PAGE_SIZE
},
{
deep: true,
immediate: true,
}
)
</script>
<style scoped lang="scss">
.table-container {
display: flex
flex-direction: column
flex: 1
.el-table {
flex: 1
}
.table-header {
display: flex
align-items: center
justify-content: space-between
.table-header-tool {
i {
margin-right: 10px
cursor: pointer
color: var(--el-text-color-regular)
&:last-of-type {
margin-right: 0
}
}
}
}
.table-divider {
margin: 0 0 20px 0
}
.table-footer {
display: flex
align-items: center
justify-content: space-between
.table-footer-total-text {
color: var(--el-text-color-regular)
font-size: 14px
}
}
}
</style>
如何使用
<template>
<div class="layout-padding">
<div class="form-adapt-container mb12">
<div class="layout-padding-view layout-padding-search">
<SearchForm
v-model="state.searchParam"
:columns="state.searchColumns"
@search="onSearch"
@reset="onReset"
/>
</div>
</div>
<div class="layout-padding-auto layout-padding-view layout-padding-table">
<CustomTable
ref="tableRef"
v-bind="state.tableParam"
:loading="loading"
:table-data="tableData"
:pages="pages"
@pageChange="onPageChange"
@sortHeader="handleSortHeader"
@refresh="onRefresh"
>
<template #operation-list>
<el-button size="default" type="primary" @click="handleShow">
<el-icon>
<ele-FolderAdd />
</el-icon>
新增角色
</el-button>
</template>
<template #operation="scope">
<el-button type="primary" text @click="handleEdit(scope.row)">编辑</el-button>
<el-divider direction="vertical" />
<el-button type="danger" text @click="handleRemove(scope.row)">删除</el-button>
</template>
</CustomTable>
</div>
<RoleDialog
ref="roleDialogRef"
:permission-list="state.permissionList"
@refresh="onRefresh"
/>
</div>
</template>
<script setup lang="ts" name="systemRole">
import { useConfirm, useTable } from "/@/hooks";
import { getRoleListApi, removeRoleApi, getPermissionListApi } from "/@/api/system";
const RoleDialog = defineAsyncComponent(
() => import("/@/views/system/role/DialogEdit.vue")
);
const tableRef = ref<HTMLElement>();
const roleDialogRef = ref<HTMLElement>();
const state = reactive<TableState>({
permissionList: [],
searchColumns: [
{
type: "input",
label: "角色名称",
prop: "nameKeyWord",
placeholder: "请输入角色名称",
span: 6,
},
],
searchParam: {
nameKeyWord: null,
},
tableParam: {
rowKey: "rowKey",
columns: [
{ prop: "name", label: "角色名称", isCheck: true },
{ prop: "description", label: "角色描述", isCheck: true },
{ prop: "createUserName", label: "创建人", isCheck: true },
{ prop: "createTime", label: "创建时间", isCheck: true },
{ prop: "operation", label: "操作", width: 160, isCustom: true, isCheck: true },
],
config: {
isBorder: false,
isSerialNo: true,
isSelection: false,
}
},
});
const {
loading,
tableData,
pages,
getTableList,
onPageChange,
onSearch,
onReset,
onRefresh,
} = useTable(getRoleListApi, state.searchParam);
const getPermissionList = async () => {
const { data } = await getPermissionListApi({});
state.permissionList = data?.root?.children ?? [];
};
const handleSortHeader = (data: TableHeaderType[]) => {
state.tableParam.columns = data;
};
const handleShow = () => {
roleDialogRef.value.onShow();
};
const handleEdit = (row: Object) => {
roleDialogRef.value.onEdit(row);
};
const handleRemove = async (row: RowRoleType) => {
const { guid } = row ?? {};
await useConfirm(
removeRoleApi,
{ guid },
`此操作将永久删除角色名称:“${row.name}”,是否继续?`
);
onRefresh();
};
onMounted(async () => {
getPermissionList();
});
</script>
<style scoped lang="scss"></style>