ct-component-plus 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ct-component-plus",
3
3
  "private": false,
4
- "version": "2.2.1",
4
+ "version": "2.2.2",
5
5
  "type": "module",
6
6
  "main": "packages/components/index.js",
7
7
  "files": [
@@ -4,6 +4,7 @@ import checkbox from './checkbox';
4
4
  import input from './input';
5
5
  import inputRange from './input-range';
6
6
  import select from './select';
7
+ import pagingSelect from './paging-select';
7
8
  import yearSelect from './year-select';
8
9
  import datePicker from './date-picker';
9
10
  import cascader from './cascader';
@@ -35,6 +36,7 @@ const components = [
35
36
  input,
36
37
  inputRange,
37
38
  select,
39
+ pagingSelect,
38
40
  yearSelect,
39
41
  datePicker,
40
42
  cascader,
@@ -79,4 +81,4 @@ export default {
79
81
  install
80
82
  }
81
83
  export { CtMessage }
82
- export * from 'element-plus'
84
+ export * from 'element-plus'
@@ -0,0 +1,8 @@
1
+ import CtPagingSelect from "./src/paging-select.vue";
2
+
3
+ /* istanbul ignore next */
4
+ CtPagingSelect.install = function (Vue) {
5
+ Vue.component("CtPagingSelect", CtPagingSelect);
6
+ };
7
+
8
+ export default CtPagingSelect;
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <ct-icon name="arrow-down_line"></ct-icon>
3
+ </template>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <ct-icon name="close_line"></ct-icon>
3
+ </template>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="ct-select__empty">
3
+ <span>{{ text }}</span>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ defineProps({
9
+ text: {
10
+ type: String,
11
+ default: "暂无数据",
12
+ },
13
+ });
14
+ </script>
@@ -0,0 +1,50 @@
1
+ import { buriedParamsKey, searchComponentProps } from "../../../hooks";
2
+ import arrowDown from "./arrow-down.vue";
3
+ import clearIcon from "./clear-icon.vue";
4
+
5
+ export const selectEmits = ["update:modelValue", buriedParamsKey];
6
+ export const selectProps = {
7
+ ...searchComponentProps,
8
+ modelValue: [String, Number, Array, Boolean],
9
+ multiple: Boolean,
10
+ filterable: Boolean,
11
+ api: String,
12
+ serviceMethod: String,
13
+ serviceParams: Object,
14
+ mapObj: {
15
+ type: Object,
16
+ default() {
17
+ return {};
18
+ },
19
+ },
20
+ pageSize: {
21
+ type: Number,
22
+ default: 50,
23
+ },
24
+ selectAllText: {
25
+ type: String,
26
+ default: "全部",
27
+ },
28
+ connectors: {
29
+ type: String,
30
+ default: "、",
31
+ },
32
+ fitInputWidth: {
33
+ type: Boolean,
34
+ default: true,
35
+ },
36
+ clearIcon: {
37
+ type: [String, Object],
38
+ default() {
39
+ return clearIcon;
40
+ },
41
+ },
42
+ suffixIcon: {
43
+ type: [String, Object],
44
+ default() {
45
+ return arrowDown;
46
+ },
47
+ },
48
+ noMatchText: String,
49
+ noDataText: String,
50
+ };
@@ -0,0 +1,490 @@
1
+ <template>
2
+ <!-- {{ rawAttr }}-- -->
3
+ <el-select
4
+ ref="selectRef"
5
+ :class="[ns.b(), ns.is('multiple', multiple)]"
6
+ v-model="valueModel"
7
+ collapse-tags
8
+ v-bind="rawAttr"
9
+ :multiple="multiple"
10
+ :filterable="filterable"
11
+ :clear-icon="clearIcon"
12
+ :suffix-icon="suffixIcon"
13
+ :fit-input-width="fitInputWidth"
14
+ :select-text="selectText"
15
+ :popper-class="popperClass"
16
+ @focus="showSearchPrefix"
17
+ @blur="hideSearchPrefix"
18
+ @click="focusSearchInput"
19
+ @visible-change="handleListSort"
20
+ >
21
+ <template #prefix>
22
+ <div
23
+ :class="[ns.e('filterable-icon')]"
24
+ v-if="filterable && showSearch && !multiple"
25
+ >
26
+ <ct-icon name="search_line"></ct-icon>
27
+ </div>
28
+ <slot name="prefix"></slot>
29
+ </template>
30
+ <div :class="[ns.e('top')]" v-if="multiple">
31
+ <div :class="[ns.e('filter')]">
32
+ <el-input v-model="keyword" ref="filterInput" @input="changeKeyword">
33
+ <template #prefix>
34
+ <ct-icon name="search_line"></ct-icon>
35
+ </template>
36
+ </el-input>
37
+ </div>
38
+ <div :class="[ns.e('select')]" v-if="!rawAttr.multipleLimit">
39
+ <span :class="[ns.e('select-title')]">
40
+ <span v-if="!keyword">已选{{ selectLength }}项</span>
41
+ <span v-else>检索结果</span>
42
+ </span>
43
+ </div>
44
+ </div>
45
+ <div
46
+ :class="[ns.e('options')]"
47
+ v-show="!noFilterOptions"
48
+ ref="optionsRef"
49
+ @scroll="handleScrollLoad"
50
+ >
51
+ <slot>
52
+ <el-option
53
+ v-for="(item, index) in filterOptions"
54
+ :key="item.value"
55
+ :label="item.label"
56
+ :value="item.value"
57
+ :disabled="item.disabled"
58
+ >
59
+ <slot name="option" :item="item" :index="index">
60
+ <span :title="selectTooltip ? item.label : undefined">{{
61
+ item.label
62
+ }}</span>
63
+ </slot>
64
+ </el-option>
65
+ </slot>
66
+ </div>
67
+ <Empty :text="emptyText" v-if="multiple && noFilterOptions" />
68
+ <template #empty>
69
+ <div :class="[ns.e('top')]" v-if="multiple">
70
+ <div :class="[ns.e('filter')]">
71
+ <el-input v-model="keyword" ref="filterInput" @input="changeKeyword">
72
+ <template #prefix>
73
+ <ct-icon name="search_line"></ct-icon>
74
+ </template>
75
+ </el-input>
76
+ </div>
77
+ <div :class="[ns.e('select')]" v-if="!rawAttr.multipleLimit">
78
+ <span :class="[ns.e('select-title')]">
79
+ <span v-if="!keyword">已选{{ selectLength }}项</span>
80
+ <span v-else>检索结果</span>
81
+ </span>
82
+ </div>
83
+ </div>
84
+ <slot name="empty">
85
+ <Empty :text="emptyText" />
86
+ </slot>
87
+ </template>
88
+ </el-select>
89
+ </template>
90
+
91
+ <script setup>
92
+ import {
93
+ onMounted,
94
+ computed,
95
+ ref,
96
+ watch,
97
+ inject,
98
+ nextTick,
99
+ useAttrs,
100
+ } from "vue";
101
+ import { selectEmits, selectProps } from "./index";
102
+ import { useNamespace, useBuriedParams } from "../../../hooks";
103
+ import { isFunction, isArray } from "../../../utils";
104
+ import Empty from "./empty.vue";
105
+ const baseDao = inject("$ctBaseDao");
106
+ const serviceConfig = inject("$ctServiceConfig");
107
+ const selectTooltip = inject("$selectTooltip");
108
+
109
+ const props = defineProps(selectProps);
110
+ const emit = defineEmits(selectEmits);
111
+ const attrs = useAttrs();
112
+
113
+ const ns = useNamespace("select");
114
+ const optionsByApi = ref([]);
115
+ const showOptions = computed(() => {
116
+ return optionsByApi.value;
117
+ });
118
+ const valueModel = computed({
119
+ get() {
120
+ return props.modelValue || (props.multiple ? [] : props.modelValue);
121
+ },
122
+ set(newValue) {
123
+ emit("update:modelValue", newValue);
124
+ },
125
+ });
126
+ const keyword = ref("");
127
+ const selectRef = ref(null);
128
+ const filterInput = ref(null);
129
+
130
+ const selectLength = computed(() => {
131
+ return valueModel.value.length;
132
+ });
133
+ const filterOptions = ref([]);
134
+ const noFilterOptions = ref(false);
135
+ const pageNo = ref(1);
136
+ const pageSize = computed(() => props.pageSize || 20);
137
+ const total = ref(0);
138
+ const loading = ref(false);
139
+ const selectObj = computed({
140
+ get() {
141
+ if (!props.multiple)
142
+ return showOptions.value.find((item) => item.value === valueModel.value);
143
+ return showOptions.value.filter((item) => {
144
+ return valueModel.value.includes(item.value);
145
+ });
146
+ },
147
+ set(newValue) {
148
+ if (!props.multiple) {
149
+ valueModel.value = newValue.value;
150
+ } else {
151
+ valueModel.value = newValue.map((item) => item.value);
152
+ }
153
+ },
154
+ });
155
+ const selectText = computed(() => {
156
+ let result = "";
157
+ if (!props.multiple) {
158
+ return result;
159
+ } else {
160
+ if (
161
+ showOptions.value.length &&
162
+ showOptions.value.length === valueModel.value.length
163
+ ) {
164
+ result = props.selectAllText;
165
+ } else {
166
+ const cnt = props.connectors;
167
+ result = selectObj.value.map((item) => item.label).join(cnt);
168
+ }
169
+ }
170
+ //nextTick(() => {
171
+ // if (selectRef.value) {
172
+ // selectRef.value.$refs.reference.input.value = result;
173
+ // }
174
+ //});
175
+ return result;
176
+ });
177
+ const emptyText = computed(() => {
178
+ return showOptions.value.length
179
+ ? props.noMatchText || "暂无匹配数据"
180
+ : props.noDataText || "暂无数据";
181
+ });
182
+
183
+ const popperClass = computed(() => {
184
+ const defaultClass = ns.e("popper");
185
+ const userClass =
186
+ attrs["popper-class"] ||
187
+ attrs["popperClass"] ||
188
+ (props.rawAttr &&
189
+ (props.rawAttr["popper-class"] || props.rawAttr["popperClass"])) ||
190
+ "";
191
+ return `${defaultClass} ${userClass}`.trim();
192
+ });
193
+
194
+ const getUseLabel = (label) => {
195
+ return typeof label === "string" ? label : String(label);
196
+ };
197
+
198
+ watch(
199
+ () => selectText.value,
200
+ (newVal) => {
201
+ if (!selectRef.value) return;
202
+ selectRef.value.selectedLabel = newVal;
203
+ }
204
+ );
205
+ watch(optionsByApi, () => {
206
+ const arr = optionsByApi.value || [];
207
+ if (arr.length) {
208
+ filterOptions.value = arr;
209
+ noFilterOptions.value = false;
210
+ } else {
211
+ filterOptions.value = [];
212
+ noFilterOptions.value = true;
213
+ }
214
+ });
215
+
216
+ const optionsRef = ref(null);
217
+ //针对多选时,已选的项要排在整个列表的最前面
218
+ const handleListSort = (val) => {
219
+ if (props.multiple) {
220
+ if (val) {
221
+ const selectedSet = new Set(valueModel.value);
222
+ filterOptions.value = [
223
+ ...filterOptions.value.filter((item) => selectedSet.has(item.value)), // 选中的项
224
+ ...filterOptions.value.filter((item) => !selectedSet.has(item.value)), // 未选中的项
225
+ ];
226
+ nextTick(() => {
227
+ optionsRef.value.scrollTop = 0;
228
+ });
229
+ } else {
230
+ keyword.value = "";
231
+ if (!valueModel.value.length) {
232
+ filterOptions.value = [...showOptions.value];
233
+ }
234
+ }
235
+ }
236
+ };
237
+ const watchServiceHandle = async (reset = false) => {
238
+ // 通过api获取数据,会监听api以及serviceParams的改变(收集到的依赖改变)都会触发重新查询
239
+ const cbs = props.cbs || {};
240
+ if (props.api && baseDao) {
241
+ try {
242
+ const method = props.serviceMethod || serviceConfig.defaultMethod;
243
+ let params = { ...(props.serviceParams || {}) };
244
+ if (reset) {
245
+ pageNo.value = 1;
246
+ optionsByApi.value = [];
247
+ }
248
+ params.keyword = keyword.value;
249
+ params.page_no = pageNo.value;
250
+ params.page_size = pageSize.value;
251
+ if (isFunction(cbs.beforeSearch)) {
252
+ const paramsHandle = await cbs.beforeSearch(params);
253
+ if (paramsHandle === false) return;
254
+ params = paramsHandle || params;
255
+ }
256
+ loading.value = true;
257
+ baseDao[method](props.api, params)
258
+ .then((res) => {
259
+ const mapObj = props.mapObj || {};
260
+ const {
261
+ list,
262
+ label = "label",
263
+ value = "value",
264
+ self,
265
+ total: totalKey = "total",
266
+ } = mapObj;
267
+ let data = [];
268
+ if (list) {
269
+ data = res[list];
270
+ } else {
271
+ data = res;
272
+ }
273
+ data = data.map((item) => {
274
+ if (self) {
275
+ return { label: getUseLabel(item), value: item };
276
+ }
277
+ return {
278
+ ...item,
279
+ label: getUseLabel(item[label]),
280
+ value: item[value],
281
+ };
282
+ });
283
+ total.value = (res && res[totalKey]) || 0;
284
+ optionsByApi.value =
285
+ pageNo.value > 1 ? optionsByApi.value.concat(data) : data;
286
+ if (isFunction(cbs.afterSearch)) {
287
+ cbs.afterSearch(res, optionsByApi, valueModel);
288
+ }
289
+ })
290
+ .finally(() => {
291
+ loading.value = false;
292
+ });
293
+ } catch (error) {
294
+ console.error(error);
295
+ }
296
+ }
297
+ if (isFunction(cbs.defineSearch)) {
298
+ try {
299
+ const defineSearchHandle = await cbs.defineSearch(
300
+ optionsByApi,
301
+ valueModel
302
+ );
303
+ if (defineSearchHandle === false) return;
304
+ if (defineSearchHandle) {
305
+ optionsByApi.value = defineSearchHandle;
306
+ }
307
+ } catch (error) {}
308
+ }
309
+ };
310
+ watch(
311
+ [
312
+ () => props.api,
313
+ () => props.serviceParams,
314
+ () => props.serviceMethod,
315
+ () => props.mapObj,
316
+ ],
317
+ (newVal, oldVal) => {
318
+ watchServiceHandle(true);
319
+ },
320
+ {
321
+ immediate: true,
322
+ }
323
+ );
324
+ // watch(
325
+ // () => keyword.value,
326
+ // () => {
327
+
328
+ // watchServiceHandle(true);
329
+ // }
330
+ // );
331
+
332
+ function debounce(fn, delay = 300) {
333
+ //防抖
334
+ var timer = null; //借助闭包
335
+ return function (...args) {
336
+ const context = this;
337
+ clearTimeout(timer);
338
+ timer = setTimeout(() => {
339
+ fn.apply(context, args);
340
+ }, delay);
341
+ };
342
+ }
343
+ const changeKeyword = debounce(() => {
344
+ watchServiceHandle(true);
345
+ });
346
+
347
+ const handleScrollLoad = (e) => {
348
+ const el = e.target;
349
+ const reach = el.scrollTop + el.clientHeight >= el.scrollHeight - 2;
350
+ const hasMore = total.value > pageNo.value * pageSize.value;
351
+ if (reach && hasMore && !loading.value) {
352
+ pageNo.value += 1;
353
+ watchServiceHandle();
354
+ }
355
+ };
356
+
357
+ const focusFilter = () => {
358
+ if (filterInput.value && filterInput.value.focus) {
359
+ filterInput.value.focus();
360
+ }
361
+ };
362
+
363
+ const showSearch = ref(false);
364
+ const focusSearchInput = () => {
365
+ setTimeout(() => {
366
+ filterInput.value && filterInput.value.focus();
367
+ // keyword.value = "";
368
+ }, 300);
369
+ };
370
+ const showSearchPrefix = () => {
371
+ showSearch.value = true;
372
+ };
373
+ const hideSearchPrefix = () => {
374
+ showSearch.value = false;
375
+ };
376
+
377
+ useBuriedParams(props, emit, {
378
+ getContent: () => {
379
+ const select = selectObj.value || {};
380
+ if (isArray(select)) {
381
+ return select.map((item) => item.label);
382
+ }
383
+ return select.label;
384
+ },
385
+ });
386
+
387
+ onMounted(() => {
388
+ if (!baseDao) {
389
+ console.error("请先配置baseDao");
390
+ }
391
+ });
392
+ defineExpose({
393
+ ref: selectRef,
394
+ keyword,
395
+ filterInput,
396
+ focusFilter,
397
+ baseDao,
398
+ serviceConfig,
399
+ noFilterOptions,
400
+ selectObj,
401
+ });
402
+ </script>
403
+ <style lang="less">
404
+ .ct-select {
405
+ width: 214px;
406
+ &.el-select {
407
+ position: relative;
408
+ }
409
+ &__top {
410
+ padding: 0 16px;
411
+ font-size: var(--ct-font-size);
412
+ }
413
+ &__options {
414
+ max-height: 274px;
415
+ overflow-y: auto;
416
+ }
417
+ &__select {
418
+ display: flex;
419
+ justify-content: space-between;
420
+ align-items: center;
421
+ margin-bottom: 10px;
422
+ &-title {
423
+ color: var(--ct-color-grey-sub);
424
+ line-height: 1;
425
+ }
426
+ .el-checkbox {
427
+ height: auto;
428
+ }
429
+ }
430
+ &__filter {
431
+ margin-bottom: 16px;
432
+ .el-input {
433
+ --el-input-height: 28px;
434
+ }
435
+ }
436
+ &__popper {
437
+ &.is-multiple {
438
+ min-width: 140px;
439
+ }
440
+ .el-select-dropdown__wrap {
441
+ max-height: unset;
442
+ }
443
+ }
444
+ .el-select__tags {
445
+ display: none;
446
+ }
447
+ .el-input__prefix-inner {
448
+ & > :last-child {
449
+ margin-right: 0;
450
+ }
451
+ }
452
+ &__filterable-icon {
453
+ position: absolute;
454
+ z-index: 3;
455
+ right: var(--ct-component-inner-padding);
456
+ top: 50%;
457
+ height: calc(var(--ct-component-size) - 2px);
458
+ transform: translateY(-50%);
459
+ background-color: #fff;
460
+ }
461
+ &__empty {
462
+ display: flex;
463
+ justify-content: center;
464
+ align-items: center;
465
+ padding: 15px 16px;
466
+ color: var(--ct-color-grey-sub);
467
+ }
468
+ &.is-multiple {
469
+ &::after {
470
+ content: attr(select-text);
471
+ position: absolute;
472
+ left: var(--ct-component-inner-padding);
473
+ right: calc(var(--ct-component-inner-padding) * 2);
474
+ top: 50%;
475
+ transform: translateY(-50%);
476
+ text-overflow: ellipsis;
477
+ overflow: hidden;
478
+ white-space: nowrap;
479
+ cursor: pointer;
480
+ pointer-events: none;
481
+ }
482
+ .el-select__placeholder.is-transparent {
483
+ display: block;
484
+ }
485
+ .el-select__selected-item {
486
+ display: none;
487
+ }
488
+ }
489
+ }
490
+ </style>