CustomSearch.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. <template>
  2. <div class="custom-search-container">
  3. <div ref="listRef" class="render-condition-list">
  4. <div v-for="(item, index) in conditions" :key="index" class="condition-item-row">
  5. <Select
  6. v-model:value="item.key"
  7. show-search
  8. placeholder="请选择字段"
  9. class="condition-field-select"
  10. :options="filterTableColumn"
  11. :field-names="{ label: 'label', value: 'prop' }"
  12. @change="() => (item.value = undefined)"
  13. />
  14. <Select
  15. v-model:value="item.condition"
  16. placeholder="请选择条件"
  17. class="condition-op-select"
  18. :options="conditionSymbols"
  19. />
  20. <template v-if="!notShowValueConditions.includes(item.condition)">
  21. <DatePicker
  22. v-if="showDatePicker(item.key)"
  23. v-model:value="item.value"
  24. show-time
  25. format="YYYY-MM-DD HH:mm:ss"
  26. value-format="YYYY-MM-DD HH:mm:ss"
  27. placeholder="选择日期"
  28. class="condition-value-input"
  29. />
  30. <Input v-else v-model:value="item.value" placeholder="请输入" allow-clear class="condition-value-input" />
  31. </template>
  32. <div v-else class="condition-value-placeholder"></div>
  33. <CloseOutlined class="remove-btn" @click="removeCondition(index)" />
  34. </div>
  35. </div>
  36. <div class="control-button-box">
  37. <Dropdown :trigger="['click']" placement="bottomLeft">
  38. <Button type="link" class="add-condition-btn">
  39. <PlusOutlined />
  40. 添加条件
  41. </Button>
  42. <template #overlay>
  43. <Menu class="condition-field-dropdown" @click="({ key }) => addCondition(key)">
  44. <MenuItem v-for="col in filterTableColumn" :key="col.prop">
  45. {{ col.label }}
  46. </MenuItem>
  47. </Menu>
  48. </template>
  49. </Dropdown>
  50. <div class="control-right">
  51. <Select v-model:value="conditionRun" class="condition-run-select" :options="conditionRunOptions" />
  52. <Button type="primary" size="small" @click="handleQuery"> 查询 </Button>
  53. <Button size="small" @click="handleReset"> 重置 </Button>
  54. </div>
  55. </div>
  56. </div>
  57. </template>
  58. <script lang="ts" setup name="CustomSearch">
  59. import { ref, computed, nextTick } from 'vue';
  60. import { Button, Select, Input, DatePicker, Dropdown, Menu, MenuItem } from 'ant-design-vue';
  61. import { PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
  62. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  63. interface TableCol {
  64. title?: string;
  65. label?: string;
  66. prop?: string;
  67. dataIndex?: string | readonly (string | number)[];
  68. [key: string]: any;
  69. }
  70. interface Condition {
  71. key: string;
  72. condition: string;
  73. value: string | undefined;
  74. }
  75. const props = defineProps<{
  76. tableColumn: TableCol[];
  77. }>();
  78. const emit = defineEmits<{
  79. (e: 'search', val: { conditionRun: string; conditions: Condition[] } | null): void;
  80. }>();
  81. const { createMessage } = useMessage();
  82. const conditionSymbols = [
  83. { label: '等于', value: '=' },
  84. { label: '不等于', value: '!=' },
  85. { label: '大于', value: '>' },
  86. { label: '大于等于', value: '>=' },
  87. { label: '小于', value: '<' },
  88. { label: '小于等于', value: '<=' },
  89. { label: '包含', value: 'LIKE' },
  90. { label: '不包含', value: 'NOT LIKE' },
  91. { label: '为空', value: 'IS NULL' },
  92. { label: '不为空', value: 'IS NOT NULL' },
  93. ];
  94. const conditionRunOptions = [
  95. { label: '满足所有条件', value: 'AND' },
  96. { label: '满足任一条件', value: 'OR' },
  97. ];
  98. const notShowValueConditions = ['IS NULL', 'IS NOT NULL', 'IS NUll', 'IS NOT NUll'];
  99. const listRef = ref<HTMLElement | null>(null);
  100. const conditions = ref<Condition[]>([]);
  101. const conditionRun = ref<string>('AND');
  102. const resolveColumnProp = (column: TableCol): string => {
  103. if (column.prop) return column.prop;
  104. if (Array.isArray(column.dataIndex)) return String(column.dataIndex[0] || '');
  105. return typeof column.dataIndex === 'string' ? column.dataIndex : '';
  106. };
  107. const filterTableColumn = computed(() => {
  108. const filterProps = ['file_name'];
  109. return props.tableColumn
  110. .filter((it) => !filterProps.includes(resolveColumnProp(it)))
  111. .map((it) => ({
  112. label: it.title ?? it.label ?? '',
  113. prop: resolveColumnProp(it),
  114. }))
  115. .filter((it) => it.prop && it.label);
  116. });
  117. const showDatePicker = (key: string) => {
  118. const col = filterTableColumn.value.find((it) => it.prop === key);
  119. const label = col?.label ?? '';
  120. const list = ['time', 'date', '时间', '日期'];
  121. const regex = new RegExp(`(${list.join('|')})`, 'gi');
  122. return regex.test(key) || (label && regex.test(label));
  123. };
  124. const addCondition = (prop: string | number) => {
  125. conditions.value.push({
  126. key: String(prop),
  127. condition: '=',
  128. value: undefined,
  129. });
  130. // 添加后自动滚动到最后一条
  131. nextTick(() => {
  132. if (listRef.value) {
  133. listRef.value.scrollTop = listRef.value.scrollHeight;
  134. }
  135. });
  136. };
  137. const removeCondition = (index: number) => {
  138. conditions.value.splice(index, 1);
  139. if (!conditions.value.length) {
  140. conditionRun.value = 'AND';
  141. emit('search', null);
  142. }
  143. };
  144. const handleQuery = () => {
  145. const hasEmpty = conditions.value.some(
  146. (it) =>
  147. !notShowValueConditions.includes(it.condition) &&
  148. (it.value === undefined || it.value === null || it.value === ''),
  149. );
  150. if (hasEmpty) {
  151. createMessage.warning('请填写查询条件的值');
  152. return;
  153. }
  154. if (!conditions.value.length) {
  155. emit('search', null);
  156. return;
  157. }
  158. emit('search', {
  159. conditionRun: conditionRun.value,
  160. conditions: conditions.value.map((it) => ({
  161. ...it,
  162. value: notShowValueConditions.includes(it.condition) ? undefined : it.value,
  163. condition: encodeURIComponent(it.condition),
  164. })),
  165. });
  166. };
  167. const resetConditions = (silent = false) => {
  168. conditions.value = [];
  169. conditionRun.value = 'AND';
  170. if (silent) return;
  171. emit('search', null);
  172. };
  173. const handleReset = () => {
  174. resetConditions(false);
  175. };
  176. const resetSearch = (options?: { silent?: boolean }) => {
  177. resetConditions(!!options?.silent);
  178. };
  179. defineExpose({ handleReset: resetSearch });
  180. </script>
  181. <style lang="less" scoped>
  182. .custom-search-container {
  183. width: 630px;
  184. box-sizing: border-box;
  185. padding: 4px 0;
  186. .render-condition-list {
  187. max-height: 360px;
  188. overflow-y: auto;
  189. margin-bottom: 10px;
  190. &::-webkit-scrollbar {
  191. width: 6px;
  192. }
  193. &::-webkit-scrollbar-thumb {
  194. background: rgba(148, 163, 184, 0.5);
  195. border-radius: 999px;
  196. }
  197. &::-webkit-scrollbar-track {
  198. background: transparent;
  199. }
  200. }
  201. .condition-item-row {
  202. display: flex;
  203. align-items: center;
  204. gap: 8px;
  205. margin-bottom: 8px;
  206. .condition-field-select {
  207. width: 160px;
  208. flex-shrink: 0;
  209. }
  210. .condition-op-select {
  211. width: 130px;
  212. flex-shrink: 0;
  213. }
  214. .condition-value-input {
  215. flex: 1;
  216. min-width: 0;
  217. }
  218. .condition-value-placeholder {
  219. flex: 1;
  220. }
  221. .remove-btn {
  222. cursor: pointer;
  223. font-size: 16px;
  224. color: #9ca3af;
  225. flex-shrink: 0;
  226. transition: color 0.15s;
  227. &:hover {
  228. color: #ef4444;
  229. }
  230. }
  231. }
  232. .control-button-box {
  233. display: flex;
  234. justify-content: space-between;
  235. align-items: center;
  236. padding-top: 4px;
  237. border-top: 1px solid #f0f0f0;
  238. .add-condition-btn {
  239. padding: 0;
  240. color: #2563eb;
  241. font-size: 13px;
  242. &:hover {
  243. color: #1d4ed8;
  244. }
  245. }
  246. .control-right {
  247. display: flex;
  248. align-items: center;
  249. gap: 8px;
  250. .condition-run-select {
  251. width: 140px;
  252. }
  253. }
  254. }
  255. }
  256. </style>
  257. <style lang="less">
  258. .condition-field-dropdown {
  259. max-height: 360px;
  260. overflow-y: auto;
  261. &::-webkit-scrollbar {
  262. width: 6px;
  263. }
  264. &::-webkit-scrollbar-thumb {
  265. background: rgba(148, 163, 184, 0.5);
  266. border-radius: 999px;
  267. }
  268. }
  269. </style>