ZmTableColumn.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <script lang="ts" setup generic="T">
  2. import type { TableColumnCtx } from 'element-plus'
  3. import { computed, useAttrs, inject, ref } from 'vue'
  4. import { Filter } from '@element-plus/icons-vue'
  5. import { SortOrder, TableContextKey } from './token'
  6. interface Props extends /* @vue-ignore */ Partial<Omit<TableColumnCtx<T>, 'prop'>> {
  7. prop?: (keyof T & string) | (string & {})
  8. zmSortable?: boolean
  9. zmFilterable?: boolean
  10. filterModelValue?: any
  11. realValue?: (value: any) => any
  12. }
  13. const emits = defineEmits(['update:filterModelValue'])
  14. const props = defineProps<Props>()
  15. const attrs = useAttrs()
  16. const tableContext = inject(TableContextKey, {
  17. data: ref([]),
  18. sortingFields: ref([]),
  19. onQuery: () => {},
  20. onSort: () => {},
  21. loading: ref(false)
  22. })
  23. const defaultOptions = ref<Partial<Props>>({
  24. align: 'center',
  25. resizable: true,
  26. showOverflowTooltip: true
  27. })
  28. const bindProps = computed(() => {
  29. const resolvedAlign =
  30. props.zmSortable || props.zmFilterable
  31. ? 'left'
  32. : attrs.align || props.align || defaultOptions.value.align
  33. return {
  34. ...defaultOptions.value,
  35. ...attrs,
  36. ...props,
  37. prop: props.prop,
  38. align: resolvedAlign,
  39. className: (props.className ?? '') + ' ' + props.prop
  40. }
  41. })
  42. const alignMap: Record<string, string> = {
  43. center: 'justify-center',
  44. left: 'justify-between',
  45. right: 'justify-end'
  46. }
  47. const headerFlexClass = computed(
  48. () => alignMap[bindProps.value.align as string] || 'justify-center'
  49. )
  50. const currentSortField = computed(() => {
  51. if (!props.prop) return undefined
  52. return tableContext.sortingFields.value.find((f: any) => f.field === props.prop)
  53. })
  54. const isSortActive = computed(() => !!currentSortField.value)
  55. const currentOrder = computed<SortOrder | undefined>(() => currentSortField.value?.order)
  56. const handleSortClick = () => {
  57. if (!props.prop) return
  58. let nextOrder: SortOrder | null = 'asc'
  59. if (currentOrder.value === 'asc') {
  60. nextOrder = 'desc'
  61. } else if (currentOrder.value === 'desc') {
  62. nextOrder = null
  63. }
  64. tableContext.onSort(props.prop, nextOrder)
  65. }
  66. const handleSearchClick = () => {
  67. if (tableContext.onQuery && props.prop) {
  68. tableContext.onQuery({
  69. prop: props.prop,
  70. value: props.filterModelValue
  71. })
  72. }
  73. }
  74. const getTextWidth = (text: string, fontSize = 14) => {
  75. const span = document.createElement('span')
  76. span.style.visibility = 'hidden'
  77. span.style.position = 'absolute'
  78. span.style.whiteSpace = 'nowrap'
  79. span.style.fontSize = `${fontSize}px`
  80. span.style.fontFamily = 'PingFang SC'
  81. span.innerText = text
  82. document.body.appendChild(span)
  83. const width = span.offsetWidth
  84. document.body.removeChild(span)
  85. return width
  86. }
  87. const calculativeWidth = () => {
  88. const values = tableContext.data.value
  89. .map((item) => props.realValue?.(item[props.prop]) || item[props.prop])
  90. .filter(Boolean)
  91. let labelWidth = getTextWidth(bindProps.value.label || '') + 38
  92. if (props.zmFilterable || props.zmSortable) {
  93. labelWidth += 8
  94. }
  95. if (props.zmFilterable) labelWidth += 22
  96. if (props.zmSortable) labelWidth += 22
  97. console.log('values :>> ', values)
  98. console.log(values.map((value) => getTextWidth(value) + 38))
  99. const maxWidth = Math.max(...values.map((value) => getTextWidth(value) + 38), labelWidth)
  100. defaultOptions.value.minWidth = maxWidth
  101. }
  102. watch(
  103. () => tableContext.loading.value,
  104. (loading) => {
  105. console.log('loading :>> ', loading)
  106. nextTick(() => {
  107. calculativeWidth()
  108. })
  109. },
  110. { immediate: true }
  111. )
  112. </script>
  113. <template>
  114. <el-table-column ref="columnRef" v-bind="bindProps">
  115. <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
  116. <slot v-if="name !== 'header'" :name="name" v-bind="slotData || {}"></slot>
  117. </template>
  118. <template #header="scope">
  119. <slot name="header" v-bind="scope">
  120. <div class="header-wrapper" :class="headerFlexClass">
  121. <span class="truncate" :title="scope.column.label">{{ scope.column.label }}</span>
  122. <div v-if="bindProps.zmSortable || bindProps.zmFilterable" class="action-area">
  123. <el-tooltip
  124. v-if="bindProps.zmSortable"
  125. :content="
  126. currentOrder === 'asc'
  127. ? '点击降序'
  128. : currentOrder === 'desc'
  129. ? '取消排序'
  130. : '点击升序'
  131. "
  132. placement="top"
  133. :show-after="500"
  134. >
  135. <div
  136. class="icon-btn"
  137. :class="{ 'is-active': isSortActive }"
  138. @click.stop="handleSortClick"
  139. >
  140. <i
  141. class="zm-sort-icon"
  142. :class="{
  143. 'is-desc': currentOrder === 'desc'
  144. }"
  145. ></i>
  146. </div>
  147. </el-tooltip>
  148. <el-popover
  149. v-if="bindProps.zmFilterable"
  150. placement="top-end"
  151. :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [16, 18] } }] }"
  152. trigger="click"
  153. :width="260"
  154. :show-arrow="false"
  155. >
  156. <template #reference>
  157. <div
  158. class="icon-btn"
  159. :class="{ 'is-active': bindProps.filterModelValue }"
  160. @click.stop
  161. >
  162. <el-icon :size="16"><Filter /></el-icon>
  163. </div>
  164. </template>
  165. <slot name="filter" v-bind="scope">
  166. <div class="flex gap-2 p-1">
  167. <el-input
  168. :model-value="bindProps.filterModelValue"
  169. @input="(val) => emits('update:filterModelValue', val)"
  170. placeholder="输入关键词"
  171. size="small"
  172. clearable
  173. @keydown.enter="handleSearchClick"
  174. />
  175. <el-button type="primary" size="small" @click="handleSearchClick">
  176. 搜索
  177. </el-button>
  178. </div>
  179. </slot>
  180. </el-popover>
  181. </div>
  182. </div>
  183. </slot>
  184. </template>
  185. </el-table-column>
  186. </template>
  187. <style scoped lang="scss">
  188. .header-wrapper {
  189. display: flex;
  190. align-items: center;
  191. width: 100%;
  192. height: 100%;
  193. user-select: none;
  194. }
  195. .action-area {
  196. display: flex;
  197. height: 100%;
  198. margin-left: 8px;
  199. align-items: center;
  200. gap: 4px;
  201. }
  202. .icon-btn {
  203. display: flex;
  204. width: 20px;
  205. height: 20px;
  206. color: var(--el-text-color-secondary);
  207. cursor: pointer;
  208. border-radius: 4px;
  209. transition: background-color 0.2s;
  210. align-items: center;
  211. justify-content: center;
  212. &:hover {
  213. color: var(--el-color-primary);
  214. background-color: var(--el-fill-color-darker);
  215. }
  216. &.is-active {
  217. color: var(--el-color-primary);
  218. }
  219. }
  220. .zm-sort-icon {
  221. position: relative;
  222. display: flex;
  223. width: 12px;
  224. height: 12px;
  225. align-items: center;
  226. justify-content: center;
  227. &::before,
  228. &::after {
  229. position: absolute;
  230. width: 8px;
  231. height: 2px;
  232. background-color: currentcolor;
  233. border-radius: 2px;
  234. content: '';
  235. transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  236. }
  237. &::before {
  238. transform: translateX(-2.2px) rotate(45deg);
  239. }
  240. &::after {
  241. transform: translateX(2.2px) rotate(-45deg);
  242. }
  243. &.is-desc {
  244. &::before {
  245. transform: translateX(-2.2px) rotate(-45deg);
  246. }
  247. &::after {
  248. transform: translateX(2.2px) rotate(45deg);
  249. }
  250. }
  251. }
  252. </style>