flameresttable 1.0.0
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/.eslintrc +27 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +28 -0
- package/.vscode/settings.json +11 -0
- package/.vscode/tasks.json +36 -0
- package/README.md +39 -0
- package/capacitor.config.json +6 -0
- package/index.html +18 -0
- package/jsconfig.json +27 -0
- package/package.json +48 -0
- package/postcss.config.js +7 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +2 -0
- package/src/App.vue +44 -0
- package/src/Table/Columns.ts +328 -0
- package/src/Table/FlameTable.ts +210 -0
- package/src/Table/Paginator.vue +55 -0
- package/src/Table/Table.vue +189 -0
- package/src/Table/TableFilters.vue +32 -0
- package/src/Table/TableOpts.ts +91 -0
- package/src/assets/icons/facebook.svg +1 -0
- package/src/assets/icons/google.svg +1 -0
- package/src/assets/logo.png +0 -0
- package/src/components/default/Modal.vue +87 -0
- package/src/components.ts +4 -0
- package/src/env.d.ts +8 -0
- package/src/index.css +3 -0
- package/src/main.ts +17 -0
- package/src/pages/Home.vue +33 -0
- package/src/pages/user/Auth.vue +90 -0
- package/src/pages/user/ResetPassword.vue +25 -0
- package/src/pages/user/ResetPasswordRequest.vue +26 -0
- package/src/pages/user/Signup.vue +96 -0
- package/src/pages/user/UserSettings.vue +25 -0
- package/src/plugin.ts +9 -0
- package/src/router.ts +40 -0
- package/src/store.ts +30 -0
- package/tailwind.config.js +73 -0
- package/tsconfig.json +51 -0
- package/tsconfig.node.json +8 -0
- package/vite.config.js +22 -0
- package/volar-fix.ps1 +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Column } from './Columns';
|
|
2
|
+
import { Rows, SavedObject } from 'flamerest';
|
|
3
|
+
import TableOpts from "./TableOpts";
|
|
4
|
+
import { reactive, UnwrapRef } from 'vue';
|
|
5
|
+
import merge from 'lodash.merge';
|
|
6
|
+
|
|
7
|
+
// Подгрузчик типа класса
|
|
8
|
+
type Class<T> = new (...args: any[]) => T
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Объект с состоянием REST таблицы
|
|
12
|
+
*/
|
|
13
|
+
export default class FlameTable<T> {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Режим таблицы
|
|
17
|
+
*/
|
|
18
|
+
public mode: "table" | "add" | "edit" = "table"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Готовый набор колонок [уже обработанный]
|
|
22
|
+
* key - имя колонки, Column - её опции
|
|
23
|
+
*/
|
|
24
|
+
public columns: { [key: string]: Column } = reactive({});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Параметры таблицы
|
|
28
|
+
*/
|
|
29
|
+
public opts: TableOpts = new TableOpts;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Инициализированная модель REST-таблицы
|
|
33
|
+
*/
|
|
34
|
+
public model: T;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Строки
|
|
38
|
+
*/
|
|
39
|
+
public Rows = reactive({ rows: [] as Array<T> });
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Параметры паджинации
|
|
43
|
+
*/
|
|
44
|
+
public Pager = reactive({
|
|
45
|
+
page: 1,
|
|
46
|
+
perPage: 20,
|
|
47
|
+
count: 0,
|
|
48
|
+
total: 0,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Параметры прогрузки
|
|
53
|
+
*/
|
|
54
|
+
public LoadParams = {} as any;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Открытая в попапе строка
|
|
58
|
+
*/
|
|
59
|
+
public OpenedRow: any = null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Проинициализировать модели
|
|
63
|
+
*/
|
|
64
|
+
public constructor(TableModel: Class<T>, opts: TableOpts) {
|
|
65
|
+
|
|
66
|
+
// Инициализируем переданный класс
|
|
67
|
+
this.model = new TableModel;
|
|
68
|
+
|
|
69
|
+
// забираем все колонки из модели и из опций
|
|
70
|
+
// так мы можем вставлять люое число колонок не привязываясь
|
|
71
|
+
// удалим так же дубликаты
|
|
72
|
+
// TODO: as any
|
|
73
|
+
const allColumns = [...Object.keys(this.model as any), ...Object.keys(opts.columnsOpts)].filter((value, index, self) => self.indexOf(value) === index)
|
|
74
|
+
|
|
75
|
+
// генерим полноценные колонки из описания модели
|
|
76
|
+
for (const key of allColumns) {
|
|
77
|
+
const newCol = new Column;
|
|
78
|
+
this.columns[key] = newCol;
|
|
79
|
+
|
|
80
|
+
// Выключаем из редактирования все праймари ключи
|
|
81
|
+
if ((this.model as any).constructor['primaryKeys'].includes(key))
|
|
82
|
+
newCol.Popup.isEnabled = false
|
|
83
|
+
|
|
84
|
+
// Мержим стандартные параметры с опциями юзера
|
|
85
|
+
merge(newCol, opts.columnsOpts[key]);
|
|
86
|
+
|
|
87
|
+
// Жёстко устанавливаем название поля из базы для этой колонки
|
|
88
|
+
newCol.name = key;
|
|
89
|
+
|
|
90
|
+
// Загружаем селекторы один раз за страницу
|
|
91
|
+
if (newCol.Popup.popupType === 'selector' && newCol.Popup.Selector.loader !== null)
|
|
92
|
+
newCol.Popup.Selector.values = reactive(newCol.Popup.Selector.loader(newCol) ?? newCol.Popup.Selector.values)
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Мержим параметры с базовой версией
|
|
97
|
+
merge(this.opts, opts);
|
|
98
|
+
|
|
99
|
+
// TODO: Сортируем колонки, если указан порядок
|
|
100
|
+
//this.columns
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
// Далее юзер просто будет делать set("колонка", опции)
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Загрузить результаты от RESTа в таблицу
|
|
110
|
+
* @param rows Отданный объект
|
|
111
|
+
*/
|
|
112
|
+
public load(rows: Rows<T>) {
|
|
113
|
+
|
|
114
|
+
if (rows.data) {
|
|
115
|
+
this.Rows.rows = (rows.data ?? []) as any;
|
|
116
|
+
Object.keys(this.Pager).forEach((key) => (this.Pager as any)[key] = (rows.pages as any)[key])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Обновить результаты от RESTа
|
|
123
|
+
* @param SaveLoadParams Сохранить стандартные параметры загрузки
|
|
124
|
+
*/
|
|
125
|
+
public async update(SaveLoadParams: any = null) {
|
|
126
|
+
|
|
127
|
+
if (SaveLoadParams !== null) this.LoadParams = SaveLoadParams;
|
|
128
|
+
|
|
129
|
+
const rows: Rows<T> = await (this.model as any).constructor.all(Object.assign(this.LoadParams, { page: this.Pager.page, perPage: this.Pager.perPage }));
|
|
130
|
+
|
|
131
|
+
this.load(rows);
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
public async add(): Promise<SavedObject<T> | null> {
|
|
138
|
+
|
|
139
|
+
// Сперва предобработка
|
|
140
|
+
// TODO: убрать пустой массив т.к. при добавлении он не нужен или прикрепить реактивную модель?
|
|
141
|
+
if (await this.opts.Popup.beforeAdd({}, this) === false) { return null; }
|
|
142
|
+
|
|
143
|
+
const res: SavedObject<T> = await Object.getPrototypeOf(this.model).constructor.create(this.getColumnsForUpdate('add'));
|
|
144
|
+
|
|
145
|
+
if (!res.ok || !res.data) {
|
|
146
|
+
throw res;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.Rows.rows.push(reactive(res.data as any))
|
|
150
|
+
|
|
151
|
+
return res;
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public async save(): Promise<SavedObject<T> | null> {
|
|
156
|
+
|
|
157
|
+
const tClass = Object.getPrototypeOf(this.model).constructor;
|
|
158
|
+
const columns = this.getColumnsForUpdate('edit');
|
|
159
|
+
const indexKey = tClass.primaryKeys[0];
|
|
160
|
+
|
|
161
|
+
// Сперва предобработка
|
|
162
|
+
if (await this.opts.Popup.beforeEdit(columns, this) === false) { return null; }
|
|
163
|
+
|
|
164
|
+
// Сохраняем
|
|
165
|
+
const res: SavedObject<T> = await tClass.edit(columns[indexKey], columns);
|
|
166
|
+
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw res;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (res.data) {
|
|
172
|
+
const findedIdx = this.Rows.rows.findIndex((row: any) => row[indexKey] === (res.data as any)[indexKey])
|
|
173
|
+
if (findedIdx !== -1)
|
|
174
|
+
this.Rows.rows[findedIdx] = reactive(res.data as any);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return res;
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
public async remove(row: T): Promise<SavedObject<T>> {
|
|
183
|
+
|
|
184
|
+
const tClass = Object.getPrototypeOf(this.model).constructor;
|
|
185
|
+
const columns = this.getColumnsForUpdate('edit');
|
|
186
|
+
const indexKey = tClass.primaryKeys[0];
|
|
187
|
+
const res = await tClass.delete((row as any)[indexKey]);
|
|
188
|
+
|
|
189
|
+
const findedIdx = this.Rows.rows.findIndex((rowx: any) => rowx[indexKey] === (row as any)[indexKey])
|
|
190
|
+
if (findedIdx !== -1)
|
|
191
|
+
this.Rows.rows.splice(findedIdx, 1);
|
|
192
|
+
|
|
193
|
+
return res;
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private getColumnsForUpdate(mode: "add" | "edit") {
|
|
198
|
+
const res: { [key: string]: string } = {};
|
|
199
|
+
for (const key in this.columns) {
|
|
200
|
+
if (mode === 'add' && !this.columns[key].Popup.isSendFromAdd) continue;
|
|
201
|
+
if (mode === 'edit' && !this.columns[key].Popup.isSendFromEdit) continue;
|
|
202
|
+
// TODO: здесь может быть полезно сохранять пустую строку
|
|
203
|
+
if (this.columns[key].Popup.model !== '') { res[key] = this.columns[key].Popup.model }
|
|
204
|
+
}
|
|
205
|
+
return res;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
nav.relative.z-0.w-full.fc.rounded-md.-space-x-px(aria-label='Pagination' @click="")
|
|
3
|
+
a.relative.inline-flex.items-center.px-2.py-2.rounded-l-md.border.border-gray-300.bg-white.text-sm.font-medium.text-gray-500(href='#' class='hover:bg-gray-50')
|
|
4
|
+
span.sr-only Previous
|
|
5
|
+
// Heroicon name: solid/chevron-left
|
|
6
|
+
svg.h-5.w-5(xmlns='http://www.w3.org/2000/svg' viewbox='0 0 20 20' fill='currentColor' aria-hidden='true')
|
|
7
|
+
path(fill-rule='evenodd' d='M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z' clip-rule='evenodd')
|
|
8
|
+
// Current: "z-10 bg-indigo-50 border-indigo-500 text-indigo-600", Default: "bg-white border-gray-300 text-gray-500 hover:bg-gray-50"
|
|
9
|
+
a.bg-white.border-gray-300.text-gray-500.relative.inline-flex.items-center.px-4.py-2.border.text-sm.font-medium(v-for="page in props.table.Pager.count" class="hover:bg-gray-50" :class="props.table.Pager.page === page ? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600' : ''" @click="goPage(page)")
|
|
10
|
+
| {{ page }}
|
|
11
|
+
//span.relative.inline-flex.items-center.px-4.py-2.border.border-gray-300.bg-white.text-sm.font-medium.text-gray-700 ...
|
|
12
|
+
a.relative.inline-flex.items-center.px-2.py-2.rounded-r-md.border.border-gray-300.bg-white.text-sm.font-medium.text-gray-500(href='#' class='hover:bg-gray-50')
|
|
13
|
+
span.sr-only Next
|
|
14
|
+
// Heroicon name: solid/chevron-right
|
|
15
|
+
svg.h-5.w-5(xmlns='http://www.w3.org/2000/svg' viewbox='0 0 20 20' fill='currentColor' aria-hidden='true')
|
|
16
|
+
path(fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import type { Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest"
|
|
23
|
+
|
|
24
|
+
// Иконки
|
|
25
|
+
import { XCircleIcon } from '@icons/24/solid'
|
|
26
|
+
import FlameTable from './FlameTable';
|
|
27
|
+
|
|
28
|
+
// Глобальное хранилище и роуты
|
|
29
|
+
const store = storeFile(), router = useRouter(), route = useRoute();
|
|
30
|
+
|
|
31
|
+
// Локальное состояние компонента
|
|
32
|
+
const state = reactive({
|
|
33
|
+
data: {}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Входящие данные компонента
|
|
37
|
+
const props = defineProps<{
|
|
38
|
+
table: typeof FlameTable.prototype
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// Примонтировано
|
|
43
|
+
onMounted(async () => {
|
|
44
|
+
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const goPage = (page: number) => {
|
|
48
|
+
// eslint-disable-next-line vue/no-mutating-props
|
|
49
|
+
props.table.Pager.page = page;
|
|
50
|
+
props.table.update();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<style scoped lang="scss"></style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.mt-0
|
|
3
|
+
|
|
4
|
+
// "ДОБАВИТЬ" И ЧИСЛО ЗАПИСЕЙ
|
|
5
|
+
.flex.items-center.justify-between
|
|
6
|
+
slot(name="defaultButtons")
|
|
7
|
+
.flex.cursor-pointer.justify-center.items-center.text-white.block.bg-blue-600.font-medium.rounded.text-sm.px-4.text-center(v-show="$props.opts?.Add.can" @click="add()" class="hover:bg-blue-700 .focus:ring-4.focus:ring-blue-200.dark:focus:ring-blue-900")
|
|
8
|
+
div.text-xl +
|
|
9
|
+
div.ml-2 {{ $props.opts?.Add.buttonTitle }}
|
|
10
|
+
slot(name="otherButtons")
|
|
11
|
+
.text-xs.text-gray-400 Показано: {{ Table.Pager.total > Table.Pager.perPage * Table.Pager.page ? Table.Pager.perPage * Table.Pager.page : Table.Pager.total }} / {{ Table.Pager.total }} записей
|
|
12
|
+
|
|
13
|
+
// ТАБЛИЦА
|
|
14
|
+
.mt-2.table.w-full
|
|
15
|
+
|
|
16
|
+
// ЗАГОЛОВКИ СТОЛБЦОВ
|
|
17
|
+
.table-header-group
|
|
18
|
+
.table-cell.border-r.border-r-slate-100.px-2(v-for="column in Table.columns" v-show="column.Table.isShow")
|
|
19
|
+
span {{ column.title !== '' ? column.title : column.name }}
|
|
20
|
+
.table-cell.border-r.border-r-slate-100.px-2(v-if="opts.Edit.can")
|
|
21
|
+
button.bg-green-600.invisible Изменить
|
|
22
|
+
.table-cell.border-r.border-r-slate-100.px-2(v-if="opts.Remove.can")
|
|
23
|
+
button.bg-green-600.invisible Удалить
|
|
24
|
+
|
|
25
|
+
// СТРОКИ
|
|
26
|
+
.table-row-group
|
|
27
|
+
.table-row.cursor-pointer(v-for="row in (Table.Rows.rows)" class="hover:bg-slate-100")
|
|
28
|
+
.table-cell.border-r.border-r-slate-100.px-2(v-for="column in Table.columns" v-show="column.Table.isShow" @click="column.Table.click(row, column)" class="last:border-r-0 last:pr-0")
|
|
29
|
+
span {{ column.Table.value(row, column) }}
|
|
30
|
+
.table-cell.border-r.border-r-slate-100.px-2.text-center(v-if="opts.Edit.can")
|
|
31
|
+
button.px-2.py-1.bg-green-600.text-white.my-1(@click="edit(row)") Изменить
|
|
32
|
+
.table-cell.border-r.border-r-slate-100.px-2.text-center(v-if="opts.Remove.can")
|
|
33
|
+
button.px-2.py-1.bg-gray-600.opacity-30.text-white.my-1(@click="deleteRow(row)") Удалить
|
|
34
|
+
|
|
35
|
+
Paginator.mt-3(:table="Table")
|
|
36
|
+
|
|
37
|
+
// МОДАЛКА ДОБАВЛЕНИЯ
|
|
38
|
+
ModalVue(ref="FlameTableModal")
|
|
39
|
+
template(v-slot)
|
|
40
|
+
.bg-white.rounded-xl(:class="'w-full desktop:w-[777px]'")
|
|
41
|
+
|
|
42
|
+
slot(name="header")
|
|
43
|
+
|
|
44
|
+
// КОЛОНКИ ТАБЛИЦЫ
|
|
45
|
+
label.flex.items-stretch.justify-center.w-full.my-1.ml-2(v-for="column in ColumnNames", v-show="Table.columns[column].Popup.isShow")
|
|
46
|
+
.w-44.text-left.px-2.py-1.bg-slate-100
|
|
47
|
+
div {{ Table.columns[column].Popup.title !== '' ? Table.columns[column].Popup.title === '' : Table.columns[column].title !== '' ? Table.columns[column].title : Table.columns[column].name }}
|
|
48
|
+
.text-xs.text-slate-400 {{ Table.columns[column].Popup.desc }}
|
|
49
|
+
.flex-1.mr-5.w-full.flex.flex-col.self-stretch(v-if="Table.columns[column].Popup.popupType === 'string' || Table.columns[column].Popup.popupType === 'text'")
|
|
50
|
+
input.h-full.self-stretch.py-1.px-2.w-full.outline-none.border.border-slate-100(v-model="Table.columns[column].Popup.model", type="text", :disabled="!Table.columns[column].Popup.isEnabled", :placeholder="Table.columns[column].Popup.placeholder !== '' ? Table.columns[column].Popup.placeholder : Table.columns[column].title !== '' ? Table.columns[column].title : Table.columns[column].name")
|
|
51
|
+
.flex-1.mr-5.w-full.flex.flex-col.self-stretch(v-if="Table.columns[column].Popup.popupType === 'date'")
|
|
52
|
+
Datepicker(v-model="Table.columns[column].Popup.model" autoApply )
|
|
53
|
+
.flex-1.mr-5.w-full.flex.flex-col.self-stretch(v-if="Table.columns[column].Popup.popupType === 'selector'")
|
|
54
|
+
select.h-full.self-stretch.py-1.px-2.w-full.outline-none.border.border-slate-100(v-model="Table.columns[column].Popup.model")
|
|
55
|
+
option(v-for="(selectorVal, selectorKey) in Table.columns[column].Popup.Selector.values" :value="getSelector(Table.columns[column].Popup.Selector.values, selectorKey, selectorVal)[1]") {{ getSelector(Table.columns[column].Popup.Selector.values, selectorKey, selectorVal)[0] }}
|
|
56
|
+
|
|
57
|
+
// КНОПКИ СОХРАНЕНИЯ
|
|
58
|
+
.flex.w-full.mt-3.mb-2.justify-between.items-center
|
|
59
|
+
.cursor-pointer.ml-2.text-lg.py-2.text-white.block.bg-gray-400.font-medium.rounded.text-sm.px-4.text-center(v-show="$props.opts?.Add.can" @click="Table.OpenedRow = null; FlameTableModal?.close()" class="hover:bg-blue-700 focus:ring-4 focus:ring-blue-200 dark:focus:ring-blue-900") Отмена
|
|
60
|
+
.cursor-pointer.ml-2.text-lg.py-2.mr-3.text-white.block.bg-blue-600.font-medium.rounded.text-sm.px-4.text-center(@click="SaveTable()" class="hover:bg-blue-700 focus:ring-4 focus:ring-blue-200 dark:focus:ring-blue-900") {{ Table.mode === 'add' ? 'Добавить' : 'Сохранить' }}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script lang="ts">
|
|
67
|
+
import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import type { Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST, { Rows } from "flamerest"
|
|
68
|
+
|
|
69
|
+
// Иконки
|
|
70
|
+
import { XCircleIcon } from '@icons/24/solid'
|
|
71
|
+
import { computed } from '@vue/reactivity';
|
|
72
|
+
import { Column } from './Columns';
|
|
73
|
+
import { defineComponent } from 'vue';
|
|
74
|
+
import TableOpts from './TableOpts';
|
|
75
|
+
import FlameTable from './FlameTable';
|
|
76
|
+
import ModalVue from './../components/default/Modal.vue';
|
|
77
|
+
import Paginator from './Paginator.vue';
|
|
78
|
+
type Class<T> = new (...args: any[]) => T;
|
|
79
|
+
|
|
80
|
+
import Datepicker from '@vuepic/vue-datepicker';
|
|
81
|
+
import '@vuepic/vue-datepicker/dist/main.css';
|
|
82
|
+
import { isNumber } from 'lodash';
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export default defineComponent({
|
|
86
|
+
components: { ModalVue, Paginator, Datepicker },
|
|
87
|
+
props: {
|
|
88
|
+
rows: {
|
|
89
|
+
default: [] as Array<any>,
|
|
90
|
+
type: Array<any>
|
|
91
|
+
},
|
|
92
|
+
opts: {
|
|
93
|
+
default: new TableOpts,
|
|
94
|
+
type: TableOpts
|
|
95
|
+
},
|
|
96
|
+
model: {
|
|
97
|
+
default: null,
|
|
98
|
+
type: Function
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
setup(props) {
|
|
102
|
+
|
|
103
|
+
const Table = new FlameTable(props.model as any, props.opts);
|
|
104
|
+
|
|
105
|
+
const FlameTableModal = ref<InstanceType<typeof ModalVue>>();
|
|
106
|
+
|
|
107
|
+
const ColumnNames = Object.keys(Table.model as any);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Добавление новой записи
|
|
111
|
+
*/
|
|
112
|
+
const add = () => {
|
|
113
|
+
Table.mode = 'add';
|
|
114
|
+
|
|
115
|
+
// Установим все поля в пустые значения
|
|
116
|
+
ColumnNames.forEach(key => {
|
|
117
|
+
Table.columns[key].Popup.model = "";
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
FlameTableModal.value?.show();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Редактирование записи
|
|
125
|
+
*/
|
|
126
|
+
const edit = async (row: any) => {
|
|
127
|
+
Table.mode = 'edit';
|
|
128
|
+
|
|
129
|
+
await Table.opts.Popup.load(row, Table);
|
|
130
|
+
|
|
131
|
+
// Установим все поля в фактические значения из таблицы
|
|
132
|
+
ColumnNames.forEach(key => {
|
|
133
|
+
Table.columns[key].Popup.model = row[key];
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
Table.OpenedRow = row;
|
|
137
|
+
|
|
138
|
+
FlameTableModal.value?.show();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const SaveTable = async () => {
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (Table.mode === 'add') {
|
|
145
|
+
await Table.add()
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await Table.save()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (Ex) { return; }
|
|
152
|
+
|
|
153
|
+
FlameTableModal.value?.close();
|
|
154
|
+
|
|
155
|
+
Table.OpenedRow = null;
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const deleteRow = async (row: any) => {
|
|
160
|
+
if (window.confirm("Удалить запись " + row + "?")) {
|
|
161
|
+
Table.remove(row);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const getSelector = (arr: any, selectorKey: any, selectorVal: any) => {
|
|
166
|
+
if (Array.isArray(arr)) return [selectorVal, selectorVal];
|
|
167
|
+
// TODO: тут загрузка объекта должна быть
|
|
168
|
+
if (typeof arr === 'object' && arr !== null) return [selectorKey, selectorVal]
|
|
169
|
+
return [selectorKey, selectorVal]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
Table,
|
|
174
|
+
FlameTableModal,
|
|
175
|
+
ColumnNames,
|
|
176
|
+
add,
|
|
177
|
+
edit,
|
|
178
|
+
SaveTable,
|
|
179
|
+
deleteRow,
|
|
180
|
+
getSelector
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<style scoped lang="scss"></style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.flex
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { onMounted, reactive, ref, defineProps } from '@vue/runtime-core'; import type { Ref } from 'vue'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest"
|
|
7
|
+
|
|
8
|
+
// Иконки
|
|
9
|
+
import { XCircleIcon } from '@icons/24/solid'
|
|
10
|
+
import { TableFilter } from './TableOpts.js';
|
|
11
|
+
|
|
12
|
+
// Глобальное хранилище и роуты
|
|
13
|
+
const store = storeFile(), router = useRouter(), route = useRoute();
|
|
14
|
+
|
|
15
|
+
// Локальное состояние компонента
|
|
16
|
+
const state = reactive({
|
|
17
|
+
data: {}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Входящие данные компонента
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
filters: Array<TableFilter>
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
// Примонтировано
|
|
26
|
+
onMounted(async () => {
|
|
27
|
+
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style scoped lang="scss"></style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Column, IColumn } from './Columns';
|
|
2
|
+
|
|
3
|
+
import merge from 'lodash.merge'
|
|
4
|
+
import FlameTable from './FlameTable';
|
|
5
|
+
|
|
6
|
+
export default class TableOpts {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Колонки
|
|
10
|
+
*/
|
|
11
|
+
public columnsOpts: { [key: string]: IColumn } = {};
|
|
12
|
+
|
|
13
|
+
public set(ColumnName: string, mergingOpts: IColumn) {
|
|
14
|
+
// TODO: ЕСТЬ ОШИБКА???
|
|
15
|
+
if (this.columnsOpts[ColumnName] === undefined) this.columnsOpts[ColumnName] = new Column as any
|
|
16
|
+
merge(this.columnsOpts[ColumnName], mergingOpts)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Удалить одну колонку или сразу несколько
|
|
21
|
+
* @param ColumnName Полностью удалить строку
|
|
22
|
+
*/
|
|
23
|
+
public delete(ColumnName: string | Array<string>) {
|
|
24
|
+
|
|
25
|
+
let arr: Array<string> = [];
|
|
26
|
+
if (typeof ColumnName === 'string') arr = [ColumnName as string];
|
|
27
|
+
else arr = ColumnName as Array<string>;
|
|
28
|
+
|
|
29
|
+
for (const key in arr) {
|
|
30
|
+
// TODO: ????? as any
|
|
31
|
+
if (this.columnsOpts[arr[key]] === undefined) this.columnsOpts[arr[key]] = new Column as any
|
|
32
|
+
merge(this.columnsOpts[arr[key]], { Table: { isShow: false }, Popup: { isShow: false } })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public Add = {
|
|
38
|
+
can: true,
|
|
39
|
+
buttonTitle: "Добавить",
|
|
40
|
+
func: () => {
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
public Remove = {
|
|
44
|
+
can: true,
|
|
45
|
+
func: () => {
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
public Edit = {
|
|
49
|
+
can: true,
|
|
50
|
+
func: () => {
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
public Popup = {
|
|
55
|
+
/**
|
|
56
|
+
* Функция при загрузке попапа
|
|
57
|
+
*/
|
|
58
|
+
load: async (row: any, table: typeof FlameTable.prototype) => {
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Выполняет код перед сохранением записи, и ждёт true для продолжения
|
|
63
|
+
* @param row
|
|
64
|
+
* @param table
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
beforeEdit: async (row: any, table: typeof FlameTable.prototype): Promise<boolean> => {
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
beforeAdd: async (row: any, table: typeof FlameTable.prototype): Promise<boolean> => {
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Запрос на поиск записей
|
|
81
|
+
*/
|
|
82
|
+
public find = () => {
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TableFilter {
|
|
90
|
+
type: "float" | "integer" | "string" | "text" | "date" | "selector"
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="67px" id="Layer_1" style="enable-background:new 0 0 67 67;" version="1.1" viewBox="0 0 67 67" width="67px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M29.765,50.32h6.744V33.998h4.499l0.596-5.624h-5.095 l0.007-2.816c0-1.466,0.14-2.253,2.244-2.253h2.812V17.68h-4.5c-5.405,0-7.307,2.729-7.307,7.317v3.377h-3.369v5.625h3.369V50.32z M34,64C17.432,64,4,50.568,4,34C4,17.431,17.432,4,34,4s30,13.431,30,30C64,50.568,50.568,64,34,64z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#3A589B;"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 508.33 508.36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:none;}.cls-2{fill:#fff;fill-rule:evenodd;}.cls-3{clip-path:url(#clip-path);}.cls-4{fill:#fbbc05;}.cls-5{fill:#ea4335;}.cls-6{fill:#34a853;}.cls-7{fill:#4285f4;}</style><clipPath id="clip-path" transform="translate(-1.83 -1.82)"><path class="cls-1" d="M392.69,221.58H253.09v57.88h80.36c-7.49,36.77-38.82,57.88-80.36,57.88a88.53,88.53,0,1,1,0-177.05A86.61,86.61,0,0,1,308.25,180l43.58-43.58C325.28,113.3,291.23,99,253.09,99a149.82,149.82,0,0,0,0,299.63c74.91,0,143-54.48,143-149.82A124.29,124.29,0,0,0,392.69,221.58Z"/></clipPath></defs><title/><path class="cls-2" d="M485,29.72c11.45,15.13,16.61,40.21,19.15,70.7,3.36,45.5,4.71,100.1,6,156.52-1.67,57.47-2.35,115.43-6,156.52-2.88,31.54-9,52.66-19.22,65.47-13,12.79-37.94,23.59-73.5,26.15-43.25,3.69-96.61,3.65-155.48,5.1-65.44-1.12-109.82-.64-156.38-5.08-36.32-2.52-60.08-13.19-74.7-26.07-10.83-14.54-14-30-17-66.24-3.75-41.85-4.58-98.56-6-155.54C4,200.57,4.13,143.44,7.9,100.74,10.43,67.05,14.42,44.4,24.65,30,39,17.77,63.48,11.69,100,8.69c50-5.85,102.06-7,155.88-6.87,55.39.09,108.56,1.67,156,6.34,32,2.56,58.48,8.07,73.07,21.56Z" transform="translate(-1.83 -1.82)"/><g class="cls-3"><path class="cls-4" d="M89.66,337.34V160.29l115.77,88.53Z" transform="translate(-1.83 -1.82)"/></g><g class="cls-3"><path class="cls-5" d="M89.66,160.29l115.77,88.53,47.67-41.54,163.43-26.56V85.38H89.66Z" transform="translate(-1.83 -1.82)"/></g><g class="cls-3"><path class="cls-6" d="M89.66,337.34,294,180.72l53.8,6.81L416.53,85.38V412.25H89.66Z" transform="translate(-1.83 -1.82)"/></g><g class="cls-3"><path class="cls-7" d="M416.53,412.25,205.43,248.82l-27.24-20.43,238.34-68.1Z" transform="translate(-1.83 -1.82)"/></g></svg>
|
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
|
|
3
|
+
// УНИВЕРСАЛЬНОЕ МОДАЛЬНОЕ ОКНО
|
|
4
|
+
Позволяет вызывать себя через show() и close()
|
|
5
|
+
|
|
6
|
+
CustomModal.overall(v-model="isShow" :close="close" style="z-index: 999999;")
|
|
7
|
+
|
|
8
|
+
// Этот слот для замены самого окна
|
|
9
|
+
slot
|
|
10
|
+
|
|
11
|
+
.bg-white.rounded-xl
|
|
12
|
+
|
|
13
|
+
// Этот слот позволяет заменить всё сразу внутри окна
|
|
14
|
+
slot(name="inner")
|
|
15
|
+
|
|
16
|
+
// Заголовок
|
|
17
|
+
.flex.items-center.px-5.border-b.border-b-gray-300(v-if="title !== ''")
|
|
18
|
+
h1.text-3xl.py-5 {{ title }}
|
|
19
|
+
XCircleIcon.ml-24.w-6.opacity-25.cursor-pointer(@click="close")
|
|
20
|
+
|
|
21
|
+
// Тело
|
|
22
|
+
.my-3.px-5
|
|
23
|
+
slot(name="body")
|
|
24
|
+
.text-left {{ body === '' ? 'Укажите сообщение или используйте слот body' : '' }}
|
|
25
|
+
|
|
26
|
+
// Кнопки
|
|
27
|
+
slot(name="buttons")
|
|
28
|
+
.border-t.border-t-gray-300.mb-4.pt-3.pr-5.flex.items-end.flex-col
|
|
29
|
+
button.cursor-pointer.ml-5.rounded-full.bg-blue-700.px-4.py-1.text-white(@click="close") {{ closeButton === '' ? 'Закрыть' : closeButton }}
|
|
30
|
+
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script lang="ts">
|
|
34
|
+
import { ref, defineComponent } from '@vue/runtime-core'; import { storeFile } from "@/store"; import { useRoute, useRouter } from 'vue-router'; import REST from "flamerest";
|
|
35
|
+
|
|
36
|
+
// Иконки
|
|
37
|
+
import { XCircleIcon } from '@icons/24/solid'
|
|
38
|
+
|
|
39
|
+
export default defineComponent({
|
|
40
|
+
|
|
41
|
+
components: { XCircleIcon },
|
|
42
|
+
|
|
43
|
+
props: {
|
|
44
|
+
title: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: "",
|
|
47
|
+
required: false
|
|
48
|
+
},
|
|
49
|
+
body: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: "",
|
|
52
|
+
required: false
|
|
53
|
+
},
|
|
54
|
+
closeButton: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: "",
|
|
57
|
+
required: false
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
setup(props) {
|
|
62
|
+
|
|
63
|
+
const isShow = ref(false)
|
|
64
|
+
|
|
65
|
+
function show() {
|
|
66
|
+
isShow.value = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function close() {
|
|
70
|
+
isShow.value = false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
isShow,
|
|
75
|
+
show,
|
|
76
|
+
close,
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style scoped lang="scss">
|
|
86
|
+
.overall {}
|
|
87
|
+
</style>
|