| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- <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
- realValue?: (value: any) => 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 = ref<Partial<Props>>({
- align: 'center',
- resizable: true,
- showOverflowTooltip: true
- })
- const bindProps = computed(() => {
- const resolvedAlign =
- props.zmSortable || props.zmFilterable
- ? 'left'
- : attrs.align || props.align || defaultOptions.value.align
- return {
- ...defaultOptions.value,
- ...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 getTextWidth = (text: string, fontSize = 14) => {
- const span = document.createElement('span')
- span.style.visibility = 'hidden'
- span.style.position = 'absolute'
- span.style.whiteSpace = 'nowrap'
- span.style.fontSize = `${fontSize}px`
- span.style.fontFamily = 'PingFang SC'
- span.innerText = text
- document.body.appendChild(span)
- const width = span.offsetWidth
- document.body.removeChild(span)
- return width
- }
- const calculativeWidth = () => {
- const values = tableContext.data.value
- .map((item) => props.realValue?.(item[props.prop]) || item[props.prop])
- .filter(Boolean)
- let labelWidth = getTextWidth(bindProps.value.label || '') + 38
- if (props.zmFilterable || props.zmSortable) {
- labelWidth += 8
- }
- if (props.zmFilterable) labelWidth += 22
- if (props.zmSortable) labelWidth += 22
- console.log('values :>> ', values)
- console.log(values.map((value) => getTextWidth(value) + 38))
- const maxWidth = Math.max(...values.map((value) => getTextWidth(value) + 38), labelWidth)
- defaultOptions.value.minWidth = maxWidth
- }
- watch(
- () => tableContext.loading.value,
- (loading) => {
- console.log('loading :>> ', 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>
|