ListPageToolbar.vue 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <template>
  2. <!-- 蓝湖风格 A:关键词 + 查询 + 多条件 + 刷新 | 操作(最多 3 个,其余进 …) -->
  3. <div class="list-page-toolbar">
  4. <div class="list-page-toolbar__left">
  5. <el-input
  6. v-if="showKeyword"
  7. :model-value="keyword"
  8. :placeholder="keywordPlaceholder"
  9. clearable
  10. class="list-page-toolbar__keyword"
  11. @update:model-value="emit('update:keyword', String($event ?? ''))"
  12. @keyup.enter="emit('search')"
  13. >
  14. <template #prefix>
  15. <el-icon><Search /></el-icon>
  16. </template>
  17. </el-input>
  18. <el-button type="primary" @click="emit('search')">查询</el-button>
  19. <el-popover
  20. v-if="$slots.filters"
  21. v-model:visible="filterOpen"
  22. placement="bottom-start"
  23. :width="filterWidth"
  24. trigger="click"
  25. >
  26. <template #reference>
  27. <el-button>
  28. 多条件查询
  29. <el-icon class="list-page-toolbar__caret"><ArrowDown /></el-icon>
  30. </el-button>
  31. </template>
  32. <div class="list-page-toolbar__filters">
  33. <slot name="filters" />
  34. <div class="list-page-toolbar__filter-footer">
  35. <el-button @click="onFilterCancel">取消</el-button>
  36. <el-button type="primary" @click="onFilterSearch">查询</el-button>
  37. </div>
  38. </div>
  39. </el-popover>
  40. <el-button
  41. class="list-page-toolbar__refresh"
  42. title="刷新"
  43. @click="emit('refresh')"
  44. >
  45. <el-icon :size="16"><RefreshRight /></el-icon>
  46. </el-button>
  47. </div>
  48. <div v-if="visibleActions.length || $slots.actions" class="list-page-toolbar__right">
  49. <slot name="actions">
  50. <el-button
  51. v-for="act in primaryActions"
  52. :key="act.key"
  53. :type="act.type || 'default'"
  54. :loading="act.loading"
  55. :disabled="act.disabled"
  56. @click="act.onClick"
  57. >
  58. {{ act.label }}
  59. </el-button>
  60. <el-dropdown v-if="moreActions.length" trigger="click">
  61. <el-button class="list-page-toolbar__more" title="更多">
  62. <el-icon :size="16"><MoreFilled /></el-icon>
  63. </el-button>
  64. <template #dropdown>
  65. <el-dropdown-menu>
  66. <el-dropdown-item
  67. v-for="act in moreActions"
  68. :key="act.key"
  69. :disabled="act.disabled || act.loading"
  70. @click="act.onClick"
  71. >
  72. {{ act.label }}
  73. </el-dropdown-item>
  74. </el-dropdown-menu>
  75. </template>
  76. </el-dropdown>
  77. </slot>
  78. </div>
  79. </div>
  80. </template>
  81. <script setup lang="ts">
  82. import { computed, ref } from 'vue'
  83. import { ArrowDown, MoreFilled, RefreshRight, Search } from '@element-plus/icons-vue'
  84. export interface ListToolbarAction {
  85. key: string
  86. label: string
  87. type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'
  88. loading?: boolean
  89. disabled?: boolean
  90. /** 默认 true */
  91. visible?: boolean
  92. onClick: () => void
  93. }
  94. const props = withDefaults(
  95. defineProps<{
  96. keyword?: string
  97. keywordPlaceholder?: string
  98. showKeyword?: boolean
  99. /** 主区最多展示几个按钮,其余进「…」 */
  100. maxActions?: number
  101. actions?: ListToolbarAction[]
  102. filterWidth?: number
  103. }>(),
  104. {
  105. keyword: '',
  106. keywordPlaceholder: '请输入关键词查询',
  107. showKeyword: true,
  108. maxActions: 3,
  109. actions: () => [],
  110. filterWidth: 360,
  111. },
  112. )
  113. const emit = defineEmits<{
  114. 'update:keyword': [value: string]
  115. search: []
  116. refresh: []
  117. 'filter-cancel': []
  118. }>()
  119. const filterOpen = ref(false)
  120. const visibleActions = computed(() =>
  121. (props.actions || []).filter((a) => a.visible !== false),
  122. )
  123. const primaryActions = computed(() =>
  124. visibleActions.value.slice(0, props.maxActions),
  125. )
  126. const moreActions = computed(() =>
  127. visibleActions.value.slice(props.maxActions),
  128. )
  129. function onFilterCancel() {
  130. filterOpen.value = false
  131. emit('filter-cancel')
  132. }
  133. function onFilterSearch() {
  134. filterOpen.value = false
  135. emit('search')
  136. }
  137. </script>
  138. <style scoped lang="less">
  139. .list-page-toolbar {
  140. display: flex;
  141. flex-wrap: wrap;
  142. align-items: center;
  143. justify-content: space-between;
  144. gap: 12px;
  145. margin-bottom: 12px;
  146. }
  147. .list-page-toolbar__left,
  148. .list-page-toolbar__right {
  149. display: flex;
  150. flex-wrap: wrap;
  151. align-items: center;
  152. gap: 8px;
  153. }
  154. .list-page-toolbar__keyword {
  155. width: 220px;
  156. }
  157. .list-page-toolbar__caret {
  158. margin-left: 4px;
  159. }
  160. .list-page-toolbar__refresh,
  161. .list-page-toolbar__more {
  162. width: 32px;
  163. padding: 0 !important;
  164. }
  165. .list-page-toolbar__filters {
  166. display: grid;
  167. gap: 12px;
  168. }
  169. .list-page-toolbar__filter-footer {
  170. display: flex;
  171. justify-content: flex-end;
  172. gap: 8px;
  173. padding-top: 4px;
  174. }
  175. :deep(.el-button) {
  176. min-height: 32px !important;
  177. height: 32px !important;
  178. }
  179. :deep(.el-input__wrapper) {
  180. min-height: 32px !important;
  181. }
  182. </style>