cbvirtua 1.0.25 → 1.0.27
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.
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="autocomplete">
|
|
3
|
+
<div class="autocomplete__box" :class="{'autocomplete__searching' : showResults}">
|
|
4
|
+
|
|
5
|
+
<img v-if="!isLoading" class="autocomplete__icon" src="../assets/search.svg">
|
|
6
|
+
<img v-else class="autocomplete__icon animate-spin" src="../assets/loading.svg">
|
|
7
|
+
|
|
8
|
+
<div class="autocomplete__inputs">
|
|
9
|
+
<input
|
|
10
|
+
v-model="display"
|
|
11
|
+
:placeholder="placeholder"
|
|
12
|
+
:disabled="disableInput"
|
|
13
|
+
:maxlength="maxlength"
|
|
14
|
+
:class="inputClass"
|
|
15
|
+
@click="search"
|
|
16
|
+
@input="search"
|
|
17
|
+
@keydown.enter="enter"
|
|
18
|
+
@keydown.tab="close"
|
|
19
|
+
@keydown.up="up"
|
|
20
|
+
@keydown.down="down"
|
|
21
|
+
@keydown.esc="close"
|
|
22
|
+
@focus="focus"
|
|
23
|
+
@blur="blur"
|
|
24
|
+
type="text"
|
|
25
|
+
autocomplete="off">
|
|
26
|
+
<input :name="name" type="hidden" :value="value">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- clearButtonIcon -->
|
|
30
|
+
<span v-show="!disableInput && !isEmpty && !isLoading && !hasError" class="autocomplete__icon autocomplete--clear" @click="clear">
|
|
31
|
+
<span v-if="clearButtonIcon" :class="clearButtonIcon"></span>
|
|
32
|
+
<img v-else src="../assets/close.svg">
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<ul v-show="showResults" class="autocomplete__results" :style="listStyle">
|
|
37
|
+
<slot name="results">
|
|
38
|
+
<!-- error -->
|
|
39
|
+
<li v-if="hasError" class="autocomplete__results__item autocomplete__results__item--error">{{ error }}</li>
|
|
40
|
+
|
|
41
|
+
<!-- results -->
|
|
42
|
+
<template v-if="!hasError">
|
|
43
|
+
<slot name="firstResult"></slot>
|
|
44
|
+
<li
|
|
45
|
+
v-for="(result, key) in results"
|
|
46
|
+
:key="key"
|
|
47
|
+
@click.prevent="select(result)"
|
|
48
|
+
class="autocomplete__results__item"
|
|
49
|
+
:class="{'autocomplete__selected' : isSelected(key) }"
|
|
50
|
+
v-html="formatDisplay(result)">
|
|
51
|
+
</li>
|
|
52
|
+
<slot name="lastResult"></slot>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<!-- no results -->
|
|
56
|
+
<li v-if="noResultMessage" class="autocomplete__results__item autocomplete__no-results">
|
|
57
|
+
<slot name="noResults">No Results.</slot>
|
|
58
|
+
</li>
|
|
59
|
+
</slot>
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script type="text/babel">
|
|
65
|
+
import debounce from 'lodash/debounce'
|
|
66
|
+
export default {
|
|
67
|
+
props: {
|
|
68
|
+
/**
|
|
69
|
+
* Data source for the results
|
|
70
|
+
* `String` is a url, typed input will be appended
|
|
71
|
+
* `Function` received typed input, and must return a string; to be used as a url
|
|
72
|
+
* `Array` and `Object` (see `results-property`) are used directly
|
|
73
|
+
*/
|
|
74
|
+
source: {
|
|
75
|
+
type: [String, Function, Array, Object],
|
|
76
|
+
required: true
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* http method
|
|
80
|
+
*/
|
|
81
|
+
method: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: 'get'
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Input placeholder
|
|
87
|
+
*/
|
|
88
|
+
placeholder: {
|
|
89
|
+
default: 'Search'
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Preset starting value
|
|
93
|
+
*/
|
|
94
|
+
initialValue: {
|
|
95
|
+
type: [String, Number]
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Preset starting display value
|
|
99
|
+
*/
|
|
100
|
+
initialDisplay: {
|
|
101
|
+
type: String
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* CSS class for the surrounding input div
|
|
105
|
+
*/
|
|
106
|
+
inputClass: {
|
|
107
|
+
type: [String, Object]
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* To disable the input
|
|
111
|
+
*/
|
|
112
|
+
disableInput: {
|
|
113
|
+
type: Boolean
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* name property of the input holding the selected value
|
|
117
|
+
*/
|
|
118
|
+
name: {
|
|
119
|
+
type: String
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* api - property of results array
|
|
123
|
+
*/
|
|
124
|
+
resultsProperty: {
|
|
125
|
+
type: String
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Results property used as the value
|
|
129
|
+
*/
|
|
130
|
+
resultsValue: {
|
|
131
|
+
type: String,
|
|
132
|
+
default: 'id'
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Results property used as the display
|
|
136
|
+
*/
|
|
137
|
+
resultsDisplay: {
|
|
138
|
+
type: [String, Function],
|
|
139
|
+
default: 'name'
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Callback to format the server data
|
|
144
|
+
*/
|
|
145
|
+
resultsFormatter: {
|
|
146
|
+
type: Function
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether to show the no results message
|
|
151
|
+
*/
|
|
152
|
+
showNoResults: {
|
|
153
|
+
type: Boolean,
|
|
154
|
+
default: true
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Additional request headers
|
|
159
|
+
*/
|
|
160
|
+
requestHeaders: {
|
|
161
|
+
type: Object
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Credentials: same-origin, include, *omit
|
|
166
|
+
*/
|
|
167
|
+
credentials: {
|
|
168
|
+
type: String
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Optional clear button icon class
|
|
173
|
+
*/
|
|
174
|
+
clearButtonIcon: {
|
|
175
|
+
type: String
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Optional max input length
|
|
180
|
+
*/
|
|
181
|
+
maxlength: {
|
|
182
|
+
type: Number
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
data () {
|
|
186
|
+
return {
|
|
187
|
+
value: null,
|
|
188
|
+
display: null,
|
|
189
|
+
results: null,
|
|
190
|
+
selectedIndex: null,
|
|
191
|
+
loading: false,
|
|
192
|
+
isFocussed: false,
|
|
193
|
+
error: null,
|
|
194
|
+
selectedId: null,
|
|
195
|
+
selectedDisplay: null,
|
|
196
|
+
eventListener: false
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
computed: {
|
|
200
|
+
showResults () {
|
|
201
|
+
return Array.isArray(this.results) || this.hasError
|
|
202
|
+
},
|
|
203
|
+
noResults () {
|
|
204
|
+
return Array.isArray(this.results) && this.results.length === 0
|
|
205
|
+
},
|
|
206
|
+
noResultMessage () {
|
|
207
|
+
return this.noResults &&
|
|
208
|
+
!this.isLoading &&
|
|
209
|
+
this.isFocussed &&
|
|
210
|
+
!this.hasError &&
|
|
211
|
+
this.showNoResults
|
|
212
|
+
},
|
|
213
|
+
isEmpty () {
|
|
214
|
+
return !this.display
|
|
215
|
+
},
|
|
216
|
+
isLoading () {
|
|
217
|
+
return this.loading === true
|
|
218
|
+
},
|
|
219
|
+
hasError () {
|
|
220
|
+
return this.error !== null
|
|
221
|
+
},
|
|
222
|
+
listStyle () {
|
|
223
|
+
if (this.isLoading) {
|
|
224
|
+
return {
|
|
225
|
+
color: '#ccc'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
methods: {
|
|
231
|
+
/**
|
|
232
|
+
* Search wrapper method
|
|
233
|
+
*/
|
|
234
|
+
search () {
|
|
235
|
+
this.selectedIndex = null
|
|
236
|
+
switch (true) {
|
|
237
|
+
case typeof this.source === 'string':
|
|
238
|
+
// No resource search with no input
|
|
239
|
+
if (!this.display || this.display.length < 1) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.resourceSearch(this.source + this.display)
|
|
244
|
+
case typeof this.source === 'function':
|
|
245
|
+
// No resource search with no input
|
|
246
|
+
if (!this.display || this.display.length < 1) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
return this.resourceSearch(this.source(this.display))
|
|
250
|
+
case Array.isArray(this.source):
|
|
251
|
+
return this.arrayLikeSearch()
|
|
252
|
+
default:
|
|
253
|
+
throw new TypeError()
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Debounce the typed search query before making http requests
|
|
259
|
+
* @param {String} url
|
|
260
|
+
*/
|
|
261
|
+
resourceSearch: debounce(function (url) {
|
|
262
|
+
if (!this.display) {
|
|
263
|
+
this.results = []
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
this.loading = true
|
|
267
|
+
this.setEventListener()
|
|
268
|
+
this.request(url)
|
|
269
|
+
}, 200),
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Make an http request for results
|
|
273
|
+
* @param {String} url
|
|
274
|
+
*/
|
|
275
|
+
request (url) {
|
|
276
|
+
let promise = fetch(url, {
|
|
277
|
+
method: this.method,
|
|
278
|
+
credentials: this.getCredentials(),
|
|
279
|
+
headers: this.getHeaders()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return promise
|
|
283
|
+
.then(response => {
|
|
284
|
+
if (response.ok) {
|
|
285
|
+
this.error = null
|
|
286
|
+
return response.json()
|
|
287
|
+
}
|
|
288
|
+
throw new Error('Network response was not ok.')
|
|
289
|
+
})
|
|
290
|
+
.then(response => {
|
|
291
|
+
this.results = this.setResults(response)
|
|
292
|
+
this.emitRequestResultEvent()
|
|
293
|
+
this.loading = false
|
|
294
|
+
})
|
|
295
|
+
.catch(error => {
|
|
296
|
+
this.error = error.message
|
|
297
|
+
this.loading = false
|
|
298
|
+
})
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set some default headers and apply user supplied headers
|
|
303
|
+
*/
|
|
304
|
+
getHeaders () {
|
|
305
|
+
const headers = {
|
|
306
|
+
'Accept': 'application/json, text/plain, */*'
|
|
307
|
+
}
|
|
308
|
+
if (this.requestHeaders) {
|
|
309
|
+
for (var prop in this.requestHeaders) {
|
|
310
|
+
headers[prop] = this.requestHeaders[prop]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return new Headers(headers)
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Set default credentials and apply user supplied value
|
|
318
|
+
*/
|
|
319
|
+
getCredentials () {
|
|
320
|
+
let credentials = 'same-origin'
|
|
321
|
+
if (this.credentials) {
|
|
322
|
+
credentials = this.credentials
|
|
323
|
+
}
|
|
324
|
+
return credentials
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Set results property from api response
|
|
329
|
+
* @param {Object|Array} response
|
|
330
|
+
* @return {Array}
|
|
331
|
+
*/
|
|
332
|
+
setResults (response) {
|
|
333
|
+
if (this.resultsFormatter) {
|
|
334
|
+
return this.resultsFormatter(response)
|
|
335
|
+
}
|
|
336
|
+
if (this.resultsProperty && response[this.resultsProperty]) {
|
|
337
|
+
return response[this.resultsProperty]
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(response)) {
|
|
340
|
+
return response
|
|
341
|
+
}
|
|
342
|
+
return []
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Emit an event based on the request results
|
|
347
|
+
*/
|
|
348
|
+
emitRequestResultEvent () {
|
|
349
|
+
if (this.results.length === 0) {
|
|
350
|
+
this.$emit('noResults', {query: this.display})
|
|
351
|
+
} else {
|
|
352
|
+
this.$emit('results', {results: this.results})
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Search in results passed via an array
|
|
358
|
+
*/
|
|
359
|
+
arrayLikeSearch () {
|
|
360
|
+
this.setEventListener()
|
|
361
|
+
if (!this.display) {
|
|
362
|
+
this.results = this.source
|
|
363
|
+
this.$emit('results', {results: this.results})
|
|
364
|
+
this.loading = false
|
|
365
|
+
return true
|
|
366
|
+
}
|
|
367
|
+
this.results = this.source.filter((item) => {
|
|
368
|
+
return this.formatDisplay(item).toLowerCase().includes(this.display.toLowerCase())
|
|
369
|
+
})
|
|
370
|
+
this.$emit('results', {results: this.results})
|
|
371
|
+
this.loading = false
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Select a result
|
|
376
|
+
* @param {Object}
|
|
377
|
+
*/
|
|
378
|
+
select (obj) {
|
|
379
|
+
if (!obj) {
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
this.value = (this.resultsValue && obj[this.resultsValue]) ? obj[this.resultsValue] : obj.id
|
|
383
|
+
this.display = this.formatDisplay(obj)
|
|
384
|
+
this.selectedDisplay = this.display
|
|
385
|
+
this.$emit('selected', {
|
|
386
|
+
value: this.value,
|
|
387
|
+
display: this.display,
|
|
388
|
+
selectedObject: obj
|
|
389
|
+
})
|
|
390
|
+
this.$emit('input', this.value)
|
|
391
|
+
this.close()
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @param {Object} obj
|
|
396
|
+
* @return {String}
|
|
397
|
+
*/
|
|
398
|
+
formatDisplay (obj) {
|
|
399
|
+
switch (typeof this.resultsDisplay) {
|
|
400
|
+
case 'function':
|
|
401
|
+
return this.resultsDisplay(obj)
|
|
402
|
+
case 'string':
|
|
403
|
+
if (!obj[this.resultsDisplay]) {
|
|
404
|
+
throw new Error(`"${this.resultsDisplay}" property expected on result but is not defined.`)
|
|
405
|
+
}
|
|
406
|
+
return obj[this.resultsDisplay]
|
|
407
|
+
default:
|
|
408
|
+
throw new TypeError()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Register the component as focussed
|
|
414
|
+
*/
|
|
415
|
+
focus () {
|
|
416
|
+
this.isFocussed = true
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Remove the focussed value
|
|
421
|
+
*/
|
|
422
|
+
blur () {
|
|
423
|
+
this.isFocussed = false
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Is this item selected?
|
|
428
|
+
* @param {Object}
|
|
429
|
+
* @return {Boolean}
|
|
430
|
+
*/
|
|
431
|
+
isSelected (key) {
|
|
432
|
+
return key === this.selectedIndex
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Focus on the previous results item
|
|
437
|
+
*/
|
|
438
|
+
up () {
|
|
439
|
+
if (this.selectedIndex === null) {
|
|
440
|
+
this.selectedIndex = this.results.length - 1
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
this.selectedIndex = (this.selectedIndex === 0) ? this.results.length - 1 : this.selectedIndex - 1
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Focus on the next results item
|
|
448
|
+
*/
|
|
449
|
+
down () {
|
|
450
|
+
if (this.selectedIndex === null) {
|
|
451
|
+
this.selectedIndex = 0
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
this.selectedIndex = (this.selectedIndex === this.results.length - 1) ? 0 : this.selectedIndex + 1
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Select an item via the keyboard
|
|
459
|
+
*/
|
|
460
|
+
enter () {
|
|
461
|
+
if (this.selectedIndex === null) {
|
|
462
|
+
this.$emit('nothingSelected', this.display)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
this.select(this.results[this.selectedIndex])
|
|
466
|
+
this.$emit('enter', this.display)
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Clear all values, results and errors
|
|
471
|
+
*/
|
|
472
|
+
clear () {
|
|
473
|
+
this.display = null
|
|
474
|
+
this.value = null
|
|
475
|
+
this.results = null
|
|
476
|
+
this.error = null
|
|
477
|
+
this.$emit('clear')
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Close the results list. If nothing was selected clear the search
|
|
482
|
+
*/
|
|
483
|
+
close () {
|
|
484
|
+
if (!this.value || !this.selectedDisplay) {
|
|
485
|
+
this.clear()
|
|
486
|
+
}
|
|
487
|
+
if (this.selectedDisplay !== this.display && this.value) {
|
|
488
|
+
this.display = this.selectedDisplay
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.results = null
|
|
492
|
+
this.error = null
|
|
493
|
+
this.removeEventListener()
|
|
494
|
+
this.$emit('close')
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Add event listener for clicks outside the results
|
|
499
|
+
*/
|
|
500
|
+
setEventListener () {
|
|
501
|
+
if (this.eventListener) {
|
|
502
|
+
return false
|
|
503
|
+
}
|
|
504
|
+
this.eventListener = true
|
|
505
|
+
document.addEventListener('click', this.clickOutsideListener, true)
|
|
506
|
+
return true
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Remove the click event listener
|
|
511
|
+
*/
|
|
512
|
+
removeEventListener () {
|
|
513
|
+
this.eventListener = false
|
|
514
|
+
document.removeEventListener('click', this.clickOutsideListener, true)
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Method invoked by the event listener
|
|
519
|
+
*/
|
|
520
|
+
clickOutsideListener (event) {
|
|
521
|
+
if (this.$el && !this.$el.contains(event.target)) {
|
|
522
|
+
this.close()
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
mounted () {
|
|
527
|
+
this.value = this.initialValue
|
|
528
|
+
this.display = this.initialDisplay
|
|
529
|
+
this.selectedDisplay = this.initialDisplay
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
</script>
|
|
533
|
+
|
|
534
|
+
<style lang="stylus">
|
|
535
|
+
.autocomplete
|
|
536
|
+
position relative
|
|
537
|
+
width 100%
|
|
538
|
+
*
|
|
539
|
+
box-sizing border-box
|
|
540
|
+
|
|
541
|
+
.autocomplete__box
|
|
542
|
+
display flex
|
|
543
|
+
align-items center
|
|
544
|
+
background #fff
|
|
545
|
+
border: 1px solid #ccc
|
|
546
|
+
border-radius 3px
|
|
547
|
+
padding 0 5px
|
|
548
|
+
|
|
549
|
+
.autocomplete__searching
|
|
550
|
+
border-radius 3px 3px 0 0
|
|
551
|
+
|
|
552
|
+
.autocomplete__inputs
|
|
553
|
+
flex-grow 1
|
|
554
|
+
padding 0 5px
|
|
555
|
+
input
|
|
556
|
+
width 100%
|
|
557
|
+
border 0
|
|
558
|
+
&:focus
|
|
559
|
+
outline none
|
|
560
|
+
|
|
561
|
+
.autocomplete--clear
|
|
562
|
+
cursor pointer
|
|
563
|
+
|
|
564
|
+
.autocomplete__results
|
|
565
|
+
margin 0
|
|
566
|
+
padding 0
|
|
567
|
+
list-style-type none
|
|
568
|
+
z-index 1000
|
|
569
|
+
position absolute
|
|
570
|
+
max-height 400px
|
|
571
|
+
overflow-y auto
|
|
572
|
+
background white
|
|
573
|
+
width 100%
|
|
574
|
+
border 1px solid #ccc
|
|
575
|
+
border-top 0
|
|
576
|
+
color black
|
|
577
|
+
|
|
578
|
+
.autocomplete__results__item--error
|
|
579
|
+
color red
|
|
580
|
+
|
|
581
|
+
.autocomplete__results__item
|
|
582
|
+
padding 7px 10px
|
|
583
|
+
cursor pointer
|
|
584
|
+
&:hover
|
|
585
|
+
background rgba(0, 180, 255, 0.075)
|
|
586
|
+
&.autocomplete__selected
|
|
587
|
+
background rgba(0, 180, 255, 0.15)
|
|
588
|
+
|
|
589
|
+
.autocomplete__icon
|
|
590
|
+
height 14px
|
|
591
|
+
width 14px
|
|
592
|
+
|
|
593
|
+
.animate-spin
|
|
594
|
+
animation spin 2s infinite linear
|
|
595
|
+
|
|
596
|
+
@keyframes spin
|
|
597
|
+
from
|
|
598
|
+
transform rotate(0deg)
|
|
599
|
+
to
|
|
600
|
+
transform rotate(360deg)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
</style>
|
package/package.json
CHANGED
|
@@ -83,3 +83,81 @@ page{
|
|
|
83
83
|
width: 120rpx;
|
|
84
84
|
}
|
|
85
85
|
————————————————
|
|
86
|
+
|
|
87
|
+
import { getDicts as getDicDataList } from '@/api/system/dict/data'
|
|
88
|
+
|
|
89
|
+
const state = {
|
|
90
|
+
dictDataList: {},
|
|
91
|
+
requestDictList: new Set()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const getters = {
|
|
95
|
+
getDictLabel: (state) => (dicType, dictValue) => {
|
|
96
|
+
const dictList = state.dictDataList[dicType] || []
|
|
97
|
+
if(dictValue && dictValue.includes(",")){
|
|
98
|
+
const arr = val.split(",");
|
|
99
|
+
return arr.map(item => dictList.find(o => String(o.dicItemCode) === String(item))).join(",");
|
|
100
|
+
}
|
|
101
|
+
const dictItem = dictList.find(item => String(item.dicItemCode) === String(dictValue)) || {}
|
|
102
|
+
return dictItem.dicItemName || dictValue
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mutations = {
|
|
107
|
+
setDictDataList (state, dictDataList) {
|
|
108
|
+
state.dictDataList = { ...state.dictDataList, ...dictDataList }
|
|
109
|
+
},
|
|
110
|
+
pushRequestDictList (state, dictType) {
|
|
111
|
+
state.requestDictList.add(dictType)
|
|
112
|
+
},
|
|
113
|
+
spliceRequestDictList (state, dictType) {
|
|
114
|
+
state.requestDictList.delete(dictType)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const actions = {
|
|
119
|
+
async getDictData ({ commit, state }, dictType) {
|
|
120
|
+
if (Array.isArray(dictType)) {
|
|
121
|
+
// 多个数据字典请求
|
|
122
|
+
const dicTypesRequest = dictType.filter(dictType => !state.dictDataList[dictType])
|
|
123
|
+
if (dicTypesRequest.length === 0) {
|
|
124
|
+
return state.dictDataList
|
|
125
|
+
}
|
|
126
|
+
const { data } = await getDicDataList(dicTypesRequest)
|
|
127
|
+
commit('setDictDataList', data)
|
|
128
|
+
return state.dictDataList
|
|
129
|
+
} else if (typeof dictType === 'string') {
|
|
130
|
+
if (!state.dictDataList[dictType]) {
|
|
131
|
+
if (!state.requestDictList.has(dictType)) {
|
|
132
|
+
commit('pushRequestDictList', dictType)
|
|
133
|
+
const { data } = await getDicDataList(dictType)
|
|
134
|
+
commit('setDictDataList', { [dictType]: data || [] })
|
|
135
|
+
commit('spliceRequestDictList', dictType)
|
|
136
|
+
return data
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
return state.dictDataList[dictType]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
updateDictData({ commit, state }, dictType) {
|
|
144
|
+
if (!state.requestDictList.has(dictType)) {
|
|
145
|
+
commit('pushRequestDictList', dictType)
|
|
146
|
+
return getDicDataList(dictType).then(({ data }) => {
|
|
147
|
+
commit('setDictDataList', { [dictType]: data || [] })
|
|
148
|
+
return data
|
|
149
|
+
}).finally(() => {
|
|
150
|
+
commit('spliceRequestDictList', dictType)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default {
|
|
157
|
+
namespaced: true,
|
|
158
|
+
state,
|
|
159
|
+
getters,
|
|
160
|
+
mutations,
|
|
161
|
+
actions
|
|
162
|
+
}
|
|
163
|
+
|