ZmTableColumn.vue 7.4 KB

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