flameresttable 1.0.28 → 1.0.30
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 +6 -4
- package/src/App.vue +2 -1
- package/src/Table/Columns.ts +62 -6
- package/src/Table/FlameTable.ts +77 -4
- package/src/Table/Table.vue +1 -0
- package/src/Table/TableFilters.vue +39 -11
- package/src/Table/TableSelectorItem.ts +9 -0
- package/src/main.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flameresttable",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"main": "./src/plugin.ts",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,15 +23,17 @@
|
|
|
23
23
|
"pug-lexer": "github:FlameArt/pug-lexer-only",
|
|
24
24
|
"sass": "^1.69.5",
|
|
25
25
|
"vue": "^3.3.7",
|
|
26
|
-
"vue-router": "^4.2.5"
|
|
26
|
+
"vue-router": "^4.2.5",
|
|
27
|
+
"vue-select": "^4.0.0-beta.6"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@heroicons/vue": "^2.0.18",
|
|
30
31
|
"@types/lodash-es": "^4.17.10",
|
|
31
32
|
"@types/lodash.merge": "^4.6.8",
|
|
32
33
|
"@types/node": "20.8.9",
|
|
33
|
-
"@
|
|
34
|
-
"@typescript-eslint/
|
|
34
|
+
"@types/vue-select": "^3.16.6",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
|
36
|
+
"@typescript-eslint/parser": "^6.9.1",
|
|
35
37
|
"@vitejs/plugin-vue": "^4.4.0",
|
|
36
38
|
"@volar/vue-language-plugin-pug": "^1.6.5",
|
|
37
39
|
"@vue/compiler-sfc": "^3.3.7",
|
package/src/App.vue
CHANGED
|
@@ -4,6 +4,7 @@ import Table from './Table/Table.vue';
|
|
|
4
4
|
import { ref } from 'vue';
|
|
5
5
|
import Model from '@models/Testtable';
|
|
6
6
|
import TableOpts from './Table/TableOpts';
|
|
7
|
+
import Relatedtable from '@models/Relatedtable';
|
|
7
8
|
|
|
8
9
|
const store = storeFile(), router = useRouter(), route = useRoute();
|
|
9
10
|
|
|
@@ -24,7 +25,7 @@ const opts = new TableOpts;
|
|
|
24
25
|
opts.set('name', { title: 'Название' });
|
|
25
26
|
opts.set('dt1', { title: 'Дата 1' });
|
|
26
27
|
opts.set('dt2', { title: 'Дата 2' });
|
|
27
|
-
opts.set('id', { title: '
|
|
28
|
+
opts.set('id', { title: 'ID', Selector: { model: Relatedtable }, Filter: { isShow: true, type: 'selector', selector: { multiselect: true, mode: 'vertical' } } });
|
|
28
29
|
opts.set('file', { title: 'файл', Filter: { isShow: false } });
|
|
29
30
|
opts.set("name", {
|
|
30
31
|
title: 'Название', Table:
|
package/src/Table/Columns.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ITableSelectorItem from "./TableSelectorItem";
|
|
2
|
+
|
|
1
3
|
// Подгрузчик типа класса
|
|
2
4
|
type Class<T> = new (...args: any[]) => T
|
|
3
5
|
|
|
@@ -86,24 +88,30 @@ export class Column implements IColumn {
|
|
|
86
88
|
*/
|
|
87
89
|
public Selector = {
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Нужна авто-предзагрузка значений при загрузке страницы
|
|
93
|
+
* [полная таблица]
|
|
94
|
+
*/
|
|
95
|
+
preload: true,
|
|
96
|
+
|
|
89
97
|
/**
|
|
90
98
|
* Загрузчик значений
|
|
91
|
-
* @returns
|
|
99
|
+
* @returns ITableSelectorItem[]
|
|
92
100
|
*/
|
|
93
|
-
loader: () => [],
|
|
101
|
+
loader: null as ((rows?: Array<any>) => ITableSelectorItem[]) | null,
|
|
94
102
|
|
|
95
103
|
/**
|
|
96
104
|
* Модель, с которой выгружать значения
|
|
97
105
|
*/
|
|
98
|
-
model: null as
|
|
106
|
+
model: null as any | null,
|
|
99
107
|
|
|
100
108
|
/**
|
|
101
109
|
* Здесь список готовых значений
|
|
102
110
|
*/
|
|
103
|
-
values: [],
|
|
111
|
+
values: [] as ITableSelectorItem[],
|
|
104
112
|
|
|
105
113
|
/**
|
|
106
|
-
*
|
|
114
|
+
* Кастомизация вывода отдельного элемента
|
|
107
115
|
* @param row
|
|
108
116
|
* @param col
|
|
109
117
|
* @returns
|
|
@@ -130,7 +138,10 @@ export class Column implements IColumn {
|
|
|
130
138
|
/**
|
|
131
139
|
* Список значений под выбор
|
|
132
140
|
*/
|
|
133
|
-
|
|
141
|
+
selector: {
|
|
142
|
+
mode: 'vertical' as 'vertical' | 'horizontal',
|
|
143
|
+
multiselect: true,
|
|
144
|
+
},
|
|
134
145
|
|
|
135
146
|
// Минимальное число символов в строке фильтра, после которого будет произведён запрос
|
|
136
147
|
filterMinSymbolsRequest: 0,
|
|
@@ -305,6 +316,14 @@ export interface IColumn {
|
|
|
305
316
|
filterRow?: number,
|
|
306
317
|
filterColumn?: number,
|
|
307
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Список значений под выбор
|
|
321
|
+
*/
|
|
322
|
+
selector?: {
|
|
323
|
+
mode?: 'vertical' | 'horizontal',
|
|
324
|
+
multiselect?: boolean,
|
|
325
|
+
},
|
|
326
|
+
|
|
308
327
|
/**
|
|
309
328
|
* CSS-классы, применяемые к фильтру,
|
|
310
329
|
*/
|
|
@@ -312,6 +331,43 @@ export interface IColumn {
|
|
|
312
331
|
|
|
313
332
|
},
|
|
314
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Это единые related данные для всех селекторов:
|
|
336
|
+
* внутри таблицы для замены id названий на значения
|
|
337
|
+
* селектор для фильтров
|
|
338
|
+
* селектор в попапе
|
|
339
|
+
*/
|
|
340
|
+
Selector?: {
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Загрузчик значений ручной
|
|
344
|
+
* @returns ITableSelectorItem[]
|
|
345
|
+
*/
|
|
346
|
+
loader?: ((rows?: Array<any>) => ITableSelectorItem[]) | null,
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Здесь список готовых значений
|
|
350
|
+
*/
|
|
351
|
+
values?: ITableSelectorItem[],
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Модель, с которой выгружать значения
|
|
356
|
+
*/
|
|
357
|
+
model?: Class<any> | null,
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Кастомизация вывода отдельного элемента
|
|
362
|
+
* @param row
|
|
363
|
+
* @param col
|
|
364
|
+
* @returns
|
|
365
|
+
*/
|
|
366
|
+
value?: (row: any, col: string) => string
|
|
367
|
+
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
|
|
315
371
|
Popup?: {
|
|
316
372
|
|
|
317
373
|
// Тут привязка к реальным данным
|
package/src/Table/FlameTable.ts
CHANGED
|
@@ -106,10 +106,11 @@ export default class FlameTable<T> {
|
|
|
106
106
|
// TODO: Сортируем колонки, если указан порядок
|
|
107
107
|
//this.columns
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
109
|
// Далее юзер просто будет делать set("колонка", опции)
|
|
112
110
|
|
|
111
|
+
// Предзагружаем колонки
|
|
112
|
+
this.preloadRelated()
|
|
113
|
+
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
@@ -135,6 +136,62 @@ export default class FlameTable<T> {
|
|
|
135
136
|
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
public async preloadRelated() {
|
|
140
|
+
|
|
141
|
+
const allPreloadPromises: Promise<any>[] = [];
|
|
142
|
+
for (const key in this.columns) {
|
|
143
|
+
const col = this.columns[key];
|
|
144
|
+
|
|
145
|
+
// только где предзагрузка включена
|
|
146
|
+
if (col.Selector.preload !== true) continue;
|
|
147
|
+
|
|
148
|
+
// если указана модель, то загружаем через неё
|
|
149
|
+
if (col.Selector.model !== null)
|
|
150
|
+
allPreloadPromises.push(col.Selector.model.prototype.constructor.all({
|
|
151
|
+
perPage: 1000 // лимит в 1000 ответов для уменьшения дефолтного нагруза
|
|
152
|
+
}).then((r: any) => {
|
|
153
|
+
|
|
154
|
+
// Значение загрузилось: делаем преобразования
|
|
155
|
+
|
|
156
|
+
// ключ модели
|
|
157
|
+
const pk = (col.Selector.model).prototype.constructor['primaryKeys'][0];
|
|
158
|
+
|
|
159
|
+
// Ручные
|
|
160
|
+
if (typeof col.Selector.loader === 'function') {
|
|
161
|
+
col.Selector.values.push(...col.Selector.loader(r.data))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Преобразования не найдено, попытка автоматически определить параметры
|
|
165
|
+
if (col.Selector.loader === null) {
|
|
166
|
+
const rows = r.data;
|
|
167
|
+
for (const row of rows) {
|
|
168
|
+
if (typeof row[pk] !== 'undefined') {
|
|
169
|
+
|
|
170
|
+
let name = typeof row['name'] !== 'undefined' ? row['name'].toString() : null;
|
|
171
|
+
name = name ?? (typeof row['title'] !== 'undefined' ? row['title'].toString() : null);
|
|
172
|
+
name = name ?? (typeof row['desc'] !== 'undefined' ? row['desc'].toString() : null);
|
|
173
|
+
const color = (typeof row['color'] !== 'undefined' ? row['color'].toString() : null);
|
|
174
|
+
const position = typeof row[pk] === 'number' ? row[pk] : 0;
|
|
175
|
+
|
|
176
|
+
col.Selector.values.push({
|
|
177
|
+
id: row[pk],
|
|
178
|
+
title: name,
|
|
179
|
+
label: name,
|
|
180
|
+
color: color,
|
|
181
|
+
position: position
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Начинаем процесс предзагрузки
|
|
191
|
+
await Promise.allSettled(allPreloadPromises);
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
138
195
|
/**
|
|
139
196
|
* Обновить результаты от RESTа
|
|
140
197
|
* @param SaveLoadParams Сохранить стандартные параметры загрузки
|
|
@@ -166,7 +223,6 @@ export default class FlameTable<T> {
|
|
|
166
223
|
case 'date':
|
|
167
224
|
case 'fixed':
|
|
168
225
|
case 'fulltext':
|
|
169
|
-
case 'selector':
|
|
170
226
|
case 'text':
|
|
171
227
|
customFilters.params[key] = el.valueString;
|
|
172
228
|
continue;
|
|
@@ -174,6 +230,13 @@ export default class FlameTable<T> {
|
|
|
174
230
|
case 'number':
|
|
175
231
|
customFilters.params[key] = el.valueRange;
|
|
176
232
|
continue;
|
|
233
|
+
case 'selector':
|
|
234
|
+
if (this.columns[key].Filter.selector.multiselect) {
|
|
235
|
+
customFilters.params[key] = el.valueRange;
|
|
236
|
+
}
|
|
237
|
+
else
|
|
238
|
+
customFilters.params[key] = el.valueString;
|
|
239
|
+
continue;
|
|
177
240
|
default:
|
|
178
241
|
alert('НЕИЗВЕСТНЫЙ ТИП')
|
|
179
242
|
continue;
|
|
@@ -200,7 +263,6 @@ export default class FlameTable<T> {
|
|
|
200
263
|
// Точное совпадение
|
|
201
264
|
case 'fixed':
|
|
202
265
|
case 'date':
|
|
203
|
-
case 'selector':
|
|
204
266
|
|
|
205
267
|
if (el.valueString.trim() === '') continue;
|
|
206
268
|
customFilters.where[key] = el.valueString;
|
|
@@ -218,6 +280,17 @@ export default class FlameTable<T> {
|
|
|
218
280
|
|
|
219
281
|
break;
|
|
220
282
|
|
|
283
|
+
case 'selector':
|
|
284
|
+
|
|
285
|
+
if (el.selector.multiselect) {
|
|
286
|
+
if (el.valueRange.length > 0)
|
|
287
|
+
customFilters.where[key] = ['IN', key, el.valueRange];
|
|
288
|
+
}
|
|
289
|
+
else
|
|
290
|
+
customFilters.where[key] = el.valueString;
|
|
291
|
+
|
|
292
|
+
break;
|
|
293
|
+
|
|
221
294
|
default:
|
|
222
295
|
alert('Unknown table filter type');
|
|
223
296
|
}
|
package/src/Table/Table.vue
CHANGED
|
@@ -247,6 +247,7 @@ type Class<T> = new (...args: any[]) => T;
|
|
|
247
247
|
import Datepicker from '@vuepic/vue-datepicker';
|
|
248
248
|
import '@vuepic/vue-datepicker/dist/main.css';
|
|
249
249
|
import { isNumber } from 'lodash';
|
|
250
|
+
import 'vue-select/dist/vue-select.css';
|
|
250
251
|
|
|
251
252
|
|
|
252
253
|
export default defineComponent({
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="fc flex-wrap">
|
|
3
|
-
<div v-for="(col) in props.columns" :key="col.name" v-show="col.Filter.isShow"
|
|
3
|
+
<div v-for="(col) in props.columns" :key="col.name" v-show="col.Filter.isShow"
|
|
4
|
+
:class="col.Filter.classes + (col.Filter.type === 'selector' && col.Filter.selector.mode === 'horizontal' ? ' w-full ' : '')">
|
|
4
5
|
|
|
5
|
-
<!-- ТЕКСТ -->
|
|
6
6
|
<div class="fc flex-col">
|
|
7
7
|
<div class="text-xs text-slate-400">{{ col.title.toUpperCase() ?? col.name }}</div>
|
|
8
|
+
|
|
9
|
+
<!-- ТЕКСТ -->
|
|
8
10
|
<div v-if="col.Filter.type === 'text' || col.Filter.type === 'fixed' || col.Filter.type === 'fulltext'">
|
|
9
11
|
<input class="outline-none border border-slate-500 px-2 py-1 mx-2" :placeholder="col.title ?? col.name"
|
|
10
12
|
type="text" @keyup="update()" v-model="col.Filter.valueString" />
|
|
11
13
|
</div>
|
|
14
|
+
|
|
15
|
+
<!-- СЕЛЕКТОРЫ -->
|
|
12
16
|
<div v-if="col.Filter.type === 'selector'">
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
<!-- ВЕРТИКАЛЬНЫЕ -->
|
|
18
|
+
<!--<select v-if="col.Filter.selector.mode === 'vertical'" :multiple="col.Filter.selector.multiselect"
|
|
19
|
+
class="outline-none border border-slate-500 px-2 py-1 mx-2" :placeholder="col.title ?? col.name"
|
|
20
|
+
@change="update(col)" v-model="col.Filter.valueString">
|
|
21
|
+
<option v-for="item in col.Selector.values" :key="item.id" :value="item.id">
|
|
22
|
+
{{ item.title }}
|
|
17
23
|
</option>
|
|
18
|
-
</select
|
|
24
|
+
</select>-->
|
|
25
|
+
<v-select :multiple="col.Filter.selector.multiselect" v-model="col.Filter.valueString"
|
|
26
|
+
:options="col.Selector.values" class="min-w-[150px]" @option:selected="update(col)"
|
|
27
|
+
@option:deselected="update(col)" />
|
|
19
28
|
</div>
|
|
29
|
+
<div v-if="col.Filter.type === 'selector' && col.Filter.selector.mode === 'horizontal'"
|
|
30
|
+
class="w-full text-center">
|
|
31
|
+
<!-- ГОРИЗОНТАЛЬНЫЕ -->
|
|
32
|
+
<div v-for="item in col.Selector.values" :key="item.id" class="px-2 py-1 mx-1 inline-block rounded-[30px]"
|
|
33
|
+
:style="'background-color:' + (item.color ?? 'rgb(148,163,184)')">
|
|
34
|
+
{{ item.title }}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
20
38
|
<div v-if="col.Filter.type === 'date' || col.Filter.type === 'daterange'">
|
|
21
39
|
<Datepicker v-if="col.Filter.type === 'date'" v-model="col.Filter.valueString" :auto-apply="true"
|
|
22
40
|
:enable-time-picker="false" @update:model-value="update()" :teleport="true"></Datepicker>
|
|
@@ -31,22 +49,24 @@
|
|
|
31
49
|
</template>
|
|
32
50
|
|
|
33
51
|
<script setup lang="ts" generic="T">
|
|
34
|
-
import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import type
|
|
52
|
+
import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import { computed, type Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest"
|
|
35
53
|
|
|
36
54
|
// Иконки
|
|
37
55
|
import { XCircleIcon } from '@icons/24/solid'
|
|
38
56
|
import TableOpts from './TableOpts';
|
|
39
|
-
import { Column } from './Columns';
|
|
57
|
+
import { Column, IColumn } from './Columns';
|
|
40
58
|
//import { TableFilter } from './TableOpts.js';
|
|
41
59
|
import FlameTable from './FlameTable';
|
|
42
60
|
import Datepicker from '@vuepic/vue-datepicker';
|
|
61
|
+
import vSelect from 'vue-select'
|
|
62
|
+
import ITableSelectorItem from './TableSelectorItem';
|
|
43
63
|
|
|
44
64
|
// Глобальное хранилище и роуты
|
|
45
65
|
const store = storeFile(), router = useRouter(), route = useRoute();
|
|
46
66
|
|
|
47
67
|
// Локальное состояние компонента
|
|
48
68
|
const state = reactive({
|
|
49
|
-
data: {}
|
|
69
|
+
data: {},
|
|
50
70
|
})
|
|
51
71
|
|
|
52
72
|
// Входящие данные компонента
|
|
@@ -61,12 +81,20 @@ onMounted(async () => {
|
|
|
61
81
|
})
|
|
62
82
|
|
|
63
83
|
|
|
64
|
-
const update = () => {
|
|
84
|
+
const update = (col?: Column) => {
|
|
85
|
+
|
|
86
|
+
// селектор преобразуем к v-model
|
|
87
|
+
if (col && col.Filter?.selector?.multiselect) {
|
|
88
|
+
col.Filter.valueRange = (col.Filter.valueString as any).map((r: ITableSelectorItem) => r.id);
|
|
89
|
+
}
|
|
65
90
|
|
|
66
91
|
props.table.update();
|
|
67
92
|
|
|
68
93
|
}
|
|
69
94
|
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
70
98
|
</script>
|
|
71
99
|
|
|
72
100
|
<style scoped lang="scss"></style>
|
package/src/main.ts
CHANGED
|
@@ -7,6 +7,7 @@ import router from "./router";
|
|
|
7
7
|
import 'vue-universal-modal/dist/index.css'
|
|
8
8
|
import VueUniversalModal from 'vue-universal-modal'
|
|
9
9
|
import REST from 'flamerest';
|
|
10
|
+
import vSelect from 'vue-select'
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
REST.SERVER = 'http://testrest';
|
|
@@ -15,5 +16,6 @@ REST.SERVER = 'http://testrest';
|
|
|
15
16
|
createApp(App)
|
|
16
17
|
.use(createPinia())
|
|
17
18
|
.use(router)
|
|
19
|
+
.use(vSelect as any)
|
|
18
20
|
.use(VueUniversalModal, { teleportTarget: '#my-modals', modalComponent: 'CustomModal' })
|
|
19
21
|
.mount('#app')
|