|
|
@@ -0,0 +1,263 @@
|
|
|
+<script lang="ts" setup generic="T">
|
|
|
+import type { TableColumnCtx } from 'element-plus'
|
|
|
+import { computed, useAttrs, inject, ref } from 'vue'
|
|
|
+import { Filter } from '@element-plus/icons-vue'
|
|
|
+import { SortOrder, TableContextKey } from './token'
|
|
|
+
|
|
|
+interface Props extends /* @vue-ignore */ Partial<Omit<TableColumnCtx<T>, 'prop'>> {
|
|
|
+ prop: (keyof T & string) | (string & {})
|
|
|
+ zmSortable?: boolean
|
|
|
+ zmFilterable?: boolean
|
|
|
+ filterModelValue?: any
|
|
|
+}
|
|
|
+
|
|
|
+const emits = defineEmits(['update:filterModelValue'])
|
|
|
+const props = defineProps<Props>()
|
|
|
+const attrs = useAttrs()
|
|
|
+
|
|
|
+const tableContext = inject(TableContextKey, {
|
|
|
+ data: ref([]),
|
|
|
+ sortingFields: ref([]),
|
|
|
+ onQuery: () => {},
|
|
|
+ onSort: () => {},
|
|
|
+ loading: ref(false)
|
|
|
+})
|
|
|
+
|
|
|
+const defaultOptions: Partial<Props> = {
|
|
|
+ align: 'center',
|
|
|
+ resizable: true,
|
|
|
+ showOverflowTooltip: true
|
|
|
+}
|
|
|
+
|
|
|
+const bindProps = computed(() => {
|
|
|
+ const resolvedAlign =
|
|
|
+ props.zmSortable || props.zmFilterable
|
|
|
+ ? 'left'
|
|
|
+ : attrs.align || props.align || defaultOptions.align
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...defaultOptions,
|
|
|
+ ...attrs,
|
|
|
+ ...props,
|
|
|
+ prop: props.prop,
|
|
|
+ align: resolvedAlign,
|
|
|
+ className: (props.className ?? '') + ' ' + props.prop
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const alignMap: Record<string, string> = {
|
|
|
+ center: 'justify-center',
|
|
|
+ left: 'justify-between',
|
|
|
+ right: 'justify-end'
|
|
|
+}
|
|
|
+const headerFlexClass = computed(
|
|
|
+ () => alignMap[bindProps.value.align as string] || 'justify-center'
|
|
|
+)
|
|
|
+
|
|
|
+const currentSortField = computed(() => {
|
|
|
+ if (!props.prop) return undefined
|
|
|
+ return tableContext.sortingFields.value.find((f: any) => f.field === props.prop)
|
|
|
+})
|
|
|
+
|
|
|
+const isSortActive = computed(() => !!currentSortField.value)
|
|
|
+
|
|
|
+const currentOrder = computed<SortOrder | undefined>(() => currentSortField.value?.order)
|
|
|
+
|
|
|
+const handleSortClick = () => {
|
|
|
+ if (!props.prop) return
|
|
|
+
|
|
|
+ let nextOrder: SortOrder | null = 'asc'
|
|
|
+
|
|
|
+ if (currentOrder.value === 'asc') {
|
|
|
+ nextOrder = 'desc'
|
|
|
+ } else if (currentOrder.value === 'desc') {
|
|
|
+ nextOrder = null
|
|
|
+ }
|
|
|
+
|
|
|
+ tableContext.onSort(props.prop, nextOrder)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearchClick = () => {
|
|
|
+ if (tableContext.onQuery && props.prop) {
|
|
|
+ tableContext.onQuery({
|
|
|
+ prop: props.prop,
|
|
|
+ value: props.filterModelValue
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const calculativeWidth = () => {
|
|
|
+ const nodes = document.querySelectorAll(`td.${props.prop} > .cell`)
|
|
|
+ Array.from(nodes).forEach((node) => {
|
|
|
+ console.log('node :>> ', node.childNodes[1])
|
|
|
+ })
|
|
|
+ console.log('nodes :>> ', nodes)
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => tableContext.loading.value,
|
|
|
+ (loading) => {
|
|
|
+ if (!loading) {
|
|
|
+ nextTick(() => {
|
|
|
+ calculativeWidth()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <el-table-column ref="columnRef" v-bind="bindProps">
|
|
|
+ <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
|
|
|
+ <slot v-if="name !== 'header'" :name="name" v-bind="slotData || {}"></slot>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #header="scope">
|
|
|
+ <slot name="header" v-bind="scope">
|
|
|
+ <div class="header-wrapper" :class="headerFlexClass">
|
|
|
+ <span class="truncate" :title="scope.column.label">{{ scope.column.label }}</span>
|
|
|
+ <div v-if="bindProps.zmSortable || bindProps.zmFilterable" class="action-area">
|
|
|
+ <el-tooltip
|
|
|
+ v-if="bindProps.zmSortable"
|
|
|
+ :content="
|
|
|
+ currentOrder === 'asc'
|
|
|
+ ? '点击降序'
|
|
|
+ : currentOrder === 'desc'
|
|
|
+ ? '取消排序'
|
|
|
+ : '点击升序'
|
|
|
+ "
|
|
|
+ placement="top"
|
|
|
+ :show-after="500"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="icon-btn"
|
|
|
+ :class="{ 'is-active': isSortActive }"
|
|
|
+ @click.stop="handleSortClick"
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ class="zm-sort-icon"
|
|
|
+ :class="{
|
|
|
+ 'is-desc': currentOrder === 'desc'
|
|
|
+ }"
|
|
|
+ ></i>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+
|
|
|
+ <el-popover
|
|
|
+ v-if="bindProps.zmFilterable"
|
|
|
+ placement="top-end"
|
|
|
+ :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [16, 18] } }] }"
|
|
|
+ trigger="click"
|
|
|
+ :width="260"
|
|
|
+ :show-arrow="false"
|
|
|
+ >
|
|
|
+ <template #reference>
|
|
|
+ <div
|
|
|
+ class="icon-btn"
|
|
|
+ :class="{ 'is-active': bindProps.filterModelValue }"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <el-icon :size="16"><Filter /></el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <slot name="filter" v-bind="scope">
|
|
|
+ <div class="flex gap-2 p-1">
|
|
|
+ <el-input
|
|
|
+ :model-value="bindProps.filterModelValue"
|
|
|
+ @input="(val) => emits('update:filterModelValue', val)"
|
|
|
+ placeholder="输入关键词"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ @keydown.enter="handleSearchClick"
|
|
|
+ />
|
|
|
+ <el-button type="primary" size="small" @click="handleSearchClick">
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </slot>
|
|
|
+ </el-popover>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </slot>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.header-wrapper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.action-area {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+ margin-left: 8px;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.icon-btn {
|
|
|
+ display: flex;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ background-color: var(--el-fill-color-darker);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-active {
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.zm-sort-icon {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ &::before,
|
|
|
+ &::after {
|
|
|
+ position: absolute;
|
|
|
+ width: 8px;
|
|
|
+ height: 2px;
|
|
|
+ background-color: currentcolor;
|
|
|
+ border-radius: 2px;
|
|
|
+ content: '';
|
|
|
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ transform: translateX(-2.2px) rotate(45deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ transform: translateX(2.2px) rotate(-45deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-desc {
|
|
|
+ &::before {
|
|
|
+ transform: translateX(-2.2px) rotate(-45deg);
|
|
|
+ }
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ transform: translateX(2.2px) rotate(45deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|