mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-13 00:46:39 +08:00
303 lines
8 KiB
Vue
303 lines
8 KiB
Vue
<template>
|
|
<div class="complex-table">
|
|
<div class="complex-table__header" v-if="slots.header || header">
|
|
<slot name="header">{{ header }}</slot>
|
|
</div>
|
|
<div v-if="slots.toolbar">
|
|
<slot name="toolbar"></slot>
|
|
</div>
|
|
|
|
<div class="complex-table__body">
|
|
<fu-table
|
|
v-bind="$attrs"
|
|
ref="tableRef"
|
|
@selection-change="handleSelectionChange"
|
|
:max-height="tableHeight"
|
|
@row-contextmenu="handleRightClick"
|
|
@row-click="handleRowClick"
|
|
>
|
|
<slot></slot>
|
|
<template #empty>
|
|
<slot name="empty"></slot>
|
|
</template>
|
|
</fu-table>
|
|
</div>
|
|
|
|
<div
|
|
class="complex-table__pagination flex items-center w-full sm:flex-row flex-col text-xs sm:text-sm"
|
|
v-if="props.paginationConfig"
|
|
:class="{ '!justify-between': slots.paginationLeft, '!justify-end': !slots.paginationLeft }"
|
|
>
|
|
<slot name="paginationLeft"></slot>
|
|
<slot name="pagination">
|
|
<el-pagination
|
|
v-model:current-page="paginationConfig.currentPage"
|
|
v-model:page-size="paginationConfig.pageSize"
|
|
:total="paginationConfig.total"
|
|
:page-sizes="[5, 10, 20, 50, 100, 200, 500]"
|
|
@size-change="sizeChange"
|
|
@current-change="currentChange"
|
|
:size="mobile || paginationConfig.small ? 'small' : 'default'"
|
|
:layout="
|
|
mobile || paginationConfig.small
|
|
? 'total, prev, pager, next'
|
|
: 'total, sizes, prev, pager, next, jumper'
|
|
"
|
|
/>
|
|
</slot>
|
|
</div>
|
|
|
|
<ul
|
|
v-if="rightClick.visible"
|
|
class="context-menu"
|
|
ref="menuRef"
|
|
:style="{ top: `${adjustedY}px`, left: `${adjustedX}px` }"
|
|
@click.stop
|
|
>
|
|
<li
|
|
v-for="(btn, index) in rightButtons"
|
|
:key="index"
|
|
:class="[{ disabled: disabled(btn) }, { divided: btn.divided }]"
|
|
@click="!disabled(btn) && rightButtonClick(btn)"
|
|
>
|
|
{{ btn.label }}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { GlobalStore } from '@/store';
|
|
const slots = useSlots();
|
|
|
|
defineOptions({ name: 'ComplexTable' });
|
|
export interface DropdownProps {
|
|
disabled?: any;
|
|
command?: string | number | object;
|
|
label?: string | number;
|
|
[k: string]: any;
|
|
}
|
|
|
|
const props = defineProps({
|
|
header: String,
|
|
paginationConfig: {
|
|
type: Object,
|
|
required: false,
|
|
},
|
|
heightDiff: {
|
|
type: Number,
|
|
default: 320,
|
|
},
|
|
height: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
rightButtons: {
|
|
type: Array as PropType<DropdownProps[]>,
|
|
},
|
|
});
|
|
const emit = defineEmits(['search', 'update:selects', 'update:paginationConfig']);
|
|
const globalStore = GlobalStore();
|
|
const mobile = computed(() => {
|
|
return globalStore.isMobile();
|
|
});
|
|
const tableRef = ref();
|
|
const tableHeight = ref(0);
|
|
const menuRef = ref<HTMLElement | null>(null);
|
|
|
|
const rightClick = ref({
|
|
visible: false,
|
|
left: 0,
|
|
top: 0,
|
|
currentRow: null,
|
|
});
|
|
const handleRightClick = (row, column, event) => {
|
|
clearSelects();
|
|
tableRef.value.refElTable.toggleRowSelection(row);
|
|
if (!props.rightButtons) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
rightClick.value = {
|
|
visible: true,
|
|
left: event.clientX + 5,
|
|
top: event.clientY,
|
|
currentRow: row,
|
|
};
|
|
document.addEventListener('click', closeRightClick);
|
|
};
|
|
const closeRightClick = () => {
|
|
rightClick.value.visible = false;
|
|
clearSelects();
|
|
document.removeEventListener('click', closeRightClick);
|
|
};
|
|
const disabled = computed(() => {
|
|
return function (btn: any) {
|
|
return typeof btn.disabled === 'function' ? btn.disabled(rightClick.value.currentRow) : btn.disabled;
|
|
};
|
|
});
|
|
function rightButtonClick(btn: any) {
|
|
closeRightClick();
|
|
btn.click(rightClick.value.currentRow);
|
|
}
|
|
|
|
function currentChange() {
|
|
emit('search');
|
|
}
|
|
|
|
function sizeChange() {
|
|
props.paginationConfig.currentPage = 1;
|
|
localStorage.setItem(props.paginationConfig.cacheSizeKey, props.paginationConfig.pageSize);
|
|
emit('search');
|
|
}
|
|
|
|
function handleSelectionChange(row: any) {
|
|
emit('update:selects', row);
|
|
}
|
|
|
|
function sort(prop: string, order: string) {
|
|
tableRef.value.refElTable.sort(prop, order);
|
|
}
|
|
|
|
function clearSelects() {
|
|
tableRef.value.refElTable.clearSelection();
|
|
}
|
|
|
|
function clearSort() {
|
|
tableRef.value.refElTable.clearSort();
|
|
}
|
|
|
|
const adjustedX = ref(rightClick.value.left);
|
|
const adjustedY = ref(rightClick.value.top);
|
|
|
|
watch(
|
|
() => [rightClick.value.left, rightClick.value.top],
|
|
async () => {
|
|
await nextTick();
|
|
if (!menuRef.value) return;
|
|
|
|
const menuRect = menuRef.value.getBoundingClientRect();
|
|
const windowWidth = window.innerWidth;
|
|
const windowHeight = window.innerHeight;
|
|
|
|
if (rightClick.value.left + menuRect.width > windowWidth) {
|
|
adjustedX.value = windowWidth - menuRect.width - 4;
|
|
} else {
|
|
adjustedX.value = rightClick.value.left;
|
|
}
|
|
|
|
if (rightClick.value.top + menuRect.height > windowHeight) {
|
|
adjustedY.value = windowHeight - menuRect.height - 4;
|
|
} else {
|
|
adjustedY.value = rightClick.value.top;
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
function handleRowClick(row: any, column: any, event: MouseEvent) {
|
|
if (!tableRef.value) return;
|
|
const target = event.target as HTMLElement;
|
|
|
|
if (target.closest('.el-checkbox')) return;
|
|
if (
|
|
target.closest('button') ||
|
|
target.closest('a') ||
|
|
target.closest('.el-switch') ||
|
|
target.closest('.table-link') ||
|
|
target.closest('.cursor-pointer')
|
|
) {
|
|
return;
|
|
}
|
|
tableRef.value.refElTable.toggleRowSelection(row);
|
|
}
|
|
|
|
defineExpose({
|
|
clearSelects,
|
|
sort,
|
|
clearSort,
|
|
closeRightClick,
|
|
});
|
|
|
|
function calcHeight() {
|
|
let heightDiff = props.heightDiff ?? 320;
|
|
let tabHeight = globalStore.openMenuTabs ? 48 : 0;
|
|
|
|
if (props.height) {
|
|
tableHeight.value = props.height - tabHeight;
|
|
} else {
|
|
tableHeight.value = window.innerHeight - heightDiff - tabHeight;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
calcHeight();
|
|
window.addEventListener('resize', calcHeight);
|
|
watch(
|
|
() => props.height,
|
|
() => {
|
|
calcHeight();
|
|
},
|
|
);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', calcHeight);
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@use '@/styles/mixins.scss' as *;
|
|
|
|
.complex-table {
|
|
.complex-table__header {
|
|
@include flex-row(flex-start, center);
|
|
line-height: 60px;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.complex-table__body {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.complex-table__toolbar {
|
|
@include flex-row(space-between, center);
|
|
|
|
.fu-search-bar {
|
|
width: auto;
|
|
}
|
|
}
|
|
.complex-table__pagination {
|
|
margin-top: 20px;
|
|
@include flex-row(flex-end);
|
|
}
|
|
}
|
|
.context-menu {
|
|
position: fixed;
|
|
background: var(--panel-main-bg-color-9);
|
|
border: 1px solid var(--el-border-color);
|
|
border-radius: 4px;
|
|
color: var(--el-color-primary);
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
list-style: none;
|
|
font-size: 14px;
|
|
padding: 4px 0;
|
|
margin: 0;
|
|
z-index: 9999;
|
|
min-width: 120px;
|
|
}
|
|
.context-menu li {
|
|
padding: 6px 12px;
|
|
cursor: pointer;
|
|
}
|
|
.context-menu li:hover {
|
|
background-color: var(--panel-menu-bg-color);
|
|
}
|
|
.context-menu li.disabled {
|
|
color: var(--el-border-color);
|
|
cursor: not-allowed;
|
|
}
|
|
.context-menu li.divided {
|
|
border-top: 1px solid var(--el-border-color);
|
|
}
|
|
</style>
|