shared-ritm 1.2.55 → 1.2.57
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/dist/index.css +1 -1
- package/dist/shared-ritm.es.js +3897 -3900
- package/dist/shared-ritm.umd.js +7 -7
- package/dist/types/api/types/Api_Controls.d.ts +6 -6
- package/dist/types/utils/helpers.d.ts +17 -0
- package/package.json +1 -1
- package/src/api/types/Api_Controls.ts +72 -72
- package/src/common/app-table/AppTable.vue +58 -61
- package/src/common/app-table/AppTableLayout.vue +22 -24
- package/src/common/app-table/components/TableModal.vue +123 -151
- package/src/common/app-table/components/TablePagination.vue +150 -151
- package/src/common/app-table/components/TableSearch.vue +76 -78
- package/src/common/app-table/controllers/useTableModel.ts +93 -93
- package/src/utils/helpers.ts +39 -0
|
@@ -21,7 +21,7 @@ export interface ControlMeta {
|
|
|
21
21
|
totalPages: number;
|
|
22
22
|
currentPage: number;
|
|
23
23
|
}
|
|
24
|
-
export
|
|
24
|
+
export type FiltersValue = {
|
|
25
25
|
users?: {
|
|
26
26
|
users: Api_User_Dto[];
|
|
27
27
|
};
|
|
@@ -34,18 +34,18 @@ export interface FiltersValue {
|
|
|
34
34
|
status?: {
|
|
35
35
|
status: boolean;
|
|
36
36
|
};
|
|
37
|
-
}
|
|
38
|
-
export
|
|
37
|
+
};
|
|
38
|
+
export type Api_Instrument_Dto = {
|
|
39
39
|
id: string;
|
|
40
40
|
name: string;
|
|
41
41
|
storage_id?: string;
|
|
42
|
-
}
|
|
43
|
-
export
|
|
42
|
+
};
|
|
43
|
+
export type Api_Warehouse_Dto = {
|
|
44
44
|
id: string;
|
|
45
45
|
invoice_ref_key: string;
|
|
46
46
|
inventory_number: string;
|
|
47
47
|
instrument: Api_Instrument_Dto[];
|
|
48
|
-
}
|
|
48
|
+
};
|
|
49
49
|
export type Api_ControlLogs_Dto = {
|
|
50
50
|
id: string;
|
|
51
51
|
automatically: string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сравнивает два значения на глубокое равенство.
|
|
3
|
+
* Поддерживает массивы, объекты и примитивы.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isEqual(a: any, b: any): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Нормализует значение:
|
|
8
|
+
* - Если передан массив объектов, возвращает массив `.value` или сам объект.
|
|
9
|
+
* - Если передан объект, возвращает `.value` или сам объект.
|
|
10
|
+
* - Если примитив — возвращает без изменений.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeValue(val: any): any;
|
|
13
|
+
/**
|
|
14
|
+
* Генерирует UUID v4.
|
|
15
|
+
* Используется для идентификаторов временных сущностей.
|
|
16
|
+
*/
|
|
17
|
+
export declare function uuidv4(): string;
|
package/package.json
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
export type Api_User_Dto = {
|
|
2
|
-
id: string
|
|
3
|
-
full_name: string
|
|
4
|
-
}
|
|
5
|
-
export type Api_ControlZones_Dto = {
|
|
6
|
-
id: string
|
|
7
|
-
name: string
|
|
8
|
-
inventory_number: string
|
|
9
|
-
uuid: string
|
|
10
|
-
controller_zone: string
|
|
11
|
-
users?: Api_User_Dto[]
|
|
12
|
-
}
|
|
13
|
-
export type ControlsParams = {
|
|
14
|
-
search?: string
|
|
15
|
-
page?: number
|
|
16
|
-
filter?: Record<string, string[]>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ControlMeta {
|
|
20
|
-
total: number
|
|
21
|
-
perPage: number
|
|
22
|
-
totalPages: number
|
|
23
|
-
currentPage: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
-
users?: {
|
|
28
|
-
users: Api_User_Dto[]
|
|
29
|
-
}
|
|
30
|
-
controller?: {
|
|
31
|
-
controller: Api_User_Dto[]
|
|
32
|
-
}
|
|
33
|
-
responsible?: {
|
|
34
|
-
responsible: Api_User_Dto[]
|
|
35
|
-
}
|
|
36
|
-
status?: {
|
|
37
|
-
status: boolean
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
export
|
|
41
|
-
id: string
|
|
42
|
-
name: string
|
|
43
|
-
storage_id?: string
|
|
44
|
-
}
|
|
45
|
-
export
|
|
46
|
-
id: string
|
|
47
|
-
invoice_ref_key: string
|
|
48
|
-
inventory_number: string
|
|
49
|
-
instrument: Api_Instrument_Dto[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type Api_ControlLogs_Dto = {
|
|
53
|
-
id: string
|
|
54
|
-
automatically: string
|
|
55
|
-
in_zone: string
|
|
56
|
-
uuid: string
|
|
57
|
-
controller_zone: string
|
|
58
|
-
controller: Api_User_Dto[]
|
|
59
|
-
user: Api_User_Dto[]
|
|
60
|
-
frame: Api_ControlZones_Dto[]
|
|
61
|
-
warehouse: Api_Warehouse_Dto[]
|
|
62
|
-
}
|
|
63
|
-
export type Api_New_Items = {
|
|
64
|
-
title: string
|
|
65
|
-
inventory_numbers: string
|
|
66
|
-
storage_id: string
|
|
67
|
-
}
|
|
68
|
-
export type Api_ManualEntry_Dto = {
|
|
69
|
-
user: string
|
|
70
|
-
items?: string[]
|
|
71
|
-
new_items?: Api_New_Items[]
|
|
72
|
-
}
|
|
1
|
+
export type Api_User_Dto = {
|
|
2
|
+
id: string
|
|
3
|
+
full_name: string
|
|
4
|
+
}
|
|
5
|
+
export type Api_ControlZones_Dto = {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
inventory_number: string
|
|
9
|
+
uuid: string
|
|
10
|
+
controller_zone: string
|
|
11
|
+
users?: Api_User_Dto[]
|
|
12
|
+
}
|
|
13
|
+
export type ControlsParams = {
|
|
14
|
+
search?: string
|
|
15
|
+
page?: number
|
|
16
|
+
filter?: Record<string, string[]>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ControlMeta {
|
|
20
|
+
total: number
|
|
21
|
+
perPage: number
|
|
22
|
+
totalPages: number
|
|
23
|
+
currentPage: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FiltersValue = {
|
|
27
|
+
users?: {
|
|
28
|
+
users: Api_User_Dto[]
|
|
29
|
+
}
|
|
30
|
+
controller?: {
|
|
31
|
+
controller: Api_User_Dto[]
|
|
32
|
+
}
|
|
33
|
+
responsible?: {
|
|
34
|
+
responsible: Api_User_Dto[]
|
|
35
|
+
}
|
|
36
|
+
status?: {
|
|
37
|
+
status: boolean
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export type Api_Instrument_Dto = {
|
|
41
|
+
id: string
|
|
42
|
+
name: string
|
|
43
|
+
storage_id?: string
|
|
44
|
+
}
|
|
45
|
+
export type Api_Warehouse_Dto = {
|
|
46
|
+
id: string
|
|
47
|
+
invoice_ref_key: string
|
|
48
|
+
inventory_number: string
|
|
49
|
+
instrument: Api_Instrument_Dto[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Api_ControlLogs_Dto = {
|
|
53
|
+
id: string
|
|
54
|
+
automatically: string
|
|
55
|
+
in_zone: string
|
|
56
|
+
uuid: string
|
|
57
|
+
controller_zone: string
|
|
58
|
+
controller: Api_User_Dto[]
|
|
59
|
+
user: Api_User_Dto[]
|
|
60
|
+
frame: Api_ControlZones_Dto[]
|
|
61
|
+
warehouse: Api_Warehouse_Dto[]
|
|
62
|
+
}
|
|
63
|
+
export type Api_New_Items = {
|
|
64
|
+
title: string
|
|
65
|
+
inventory_numbers: string
|
|
66
|
+
storage_id: string
|
|
67
|
+
}
|
|
68
|
+
export type Api_ManualEntry_Dto = {
|
|
69
|
+
user: string
|
|
70
|
+
items?: string[]
|
|
71
|
+
new_items?: Api_New_Items[]
|
|
72
|
+
}
|
|
@@ -1,64 +1,3 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { defineProps, defineEmits, ref, computed, watch } from 'vue'
|
|
3
|
-
import type { Ref } from 'vue'
|
|
4
|
-
import FilterIcon from '@/icons/components/table-filter-icon.vue'
|
|
5
|
-
|
|
6
|
-
interface FilterOption {
|
|
7
|
-
id: string
|
|
8
|
-
name: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface TableEmits {
|
|
12
|
-
'toggle-filter-value': [colName: string, value: string]
|
|
13
|
-
'clear-filter': [colName: string]
|
|
14
|
-
'open-filter-menu': [colName: string, isOpen: boolean]
|
|
15
|
-
'row-click': [row: Record<string, any>]
|
|
16
|
-
'update:selectedRows': [rows: any[]]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const emit = defineEmits<TableEmits>()
|
|
20
|
-
|
|
21
|
-
const props = defineProps<{
|
|
22
|
-
rows: Ref<any[]>
|
|
23
|
-
columns: any[]
|
|
24
|
-
columnFilters: Ref<Record<string, string | string[] | undefined>>
|
|
25
|
-
filterMenus: Ref<Record<string, boolean>>
|
|
26
|
-
filtersOptions: Record<string, FilterOption[]>
|
|
27
|
-
meta: Ref<{ currentPage: number; perPage: number }>
|
|
28
|
-
enableMultiSelect?: boolean
|
|
29
|
-
selectedRows: any[]
|
|
30
|
-
}>()
|
|
31
|
-
|
|
32
|
-
const localSearches = ref<Record<string, string>>({})
|
|
33
|
-
|
|
34
|
-
const filteredOptions = computed(() => {
|
|
35
|
-
const result: Record<string, FilterOption[]> = {}
|
|
36
|
-
for (const col of props.columns) {
|
|
37
|
-
const search = localSearches.value[col.name]?.toLowerCase() || ''
|
|
38
|
-
const options = props.filtersOptions[col.name] || []
|
|
39
|
-
result[col.name] = options.filter(opt => opt.name.toLowerCase().includes(search))
|
|
40
|
-
}
|
|
41
|
-
return result
|
|
42
|
-
})
|
|
43
|
-
const selected = ref<any[]>([])
|
|
44
|
-
|
|
45
|
-
watch(
|
|
46
|
-
() => props.selectedRows,
|
|
47
|
-
val => {
|
|
48
|
-
selected.value = val
|
|
49
|
-
},
|
|
50
|
-
{ immediate: true },
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
watch(
|
|
54
|
-
selected,
|
|
55
|
-
val => {
|
|
56
|
-
emit('update:selectedRows', val)
|
|
57
|
-
},
|
|
58
|
-
{ deep: true },
|
|
59
|
-
)
|
|
60
|
-
</script>
|
|
61
|
-
|
|
62
1
|
<template>
|
|
63
2
|
<q-page class="flex flex-col" style="min-height: 100%">
|
|
64
3
|
<q-table
|
|
@@ -175,7 +114,65 @@ watch(
|
|
|
175
114
|
</q-table>
|
|
176
115
|
</q-page>
|
|
177
116
|
</template>
|
|
117
|
+
<script setup lang="ts">
|
|
118
|
+
import { defineProps, defineEmits, ref, computed, watch } from 'vue'
|
|
119
|
+
import type { Ref } from 'vue'
|
|
120
|
+
import FilterIcon from '@/icons/components/table-filter-icon.vue'
|
|
121
|
+
|
|
122
|
+
interface FilterOption {
|
|
123
|
+
id: string
|
|
124
|
+
name: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface TableEmits {
|
|
128
|
+
'toggle-filter-value': [colName: string, value: string]
|
|
129
|
+
'clear-filter': [colName: string]
|
|
130
|
+
'open-filter-menu': [colName: string, isOpen: boolean]
|
|
131
|
+
'row-click': [row: Record<string, any>]
|
|
132
|
+
'update:selectedRows': [rows: any[]] // синтаксис defineEmits для эмита с именованными параметрами
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const props = defineProps<{
|
|
136
|
+
rows: Ref<any[]>
|
|
137
|
+
columns: any[]
|
|
138
|
+
columnFilters: Ref<Record<string, string | string[] | undefined>>
|
|
139
|
+
filterMenus: Ref<Record<string, boolean>>
|
|
140
|
+
filtersOptions: Record<string, FilterOption[]>
|
|
141
|
+
meta: Ref<{ currentPage: number; perPage: number }>
|
|
142
|
+
enableMultiSelect?: boolean
|
|
143
|
+
selectedRows: any[]
|
|
144
|
+
}>()
|
|
145
|
+
const emit = defineEmits<TableEmits>()
|
|
178
146
|
|
|
147
|
+
const localSearches = ref<Record<string, string>>({})
|
|
148
|
+
const selected = ref<any[]>([])
|
|
149
|
+
|
|
150
|
+
const filteredOptions = computed(() => {
|
|
151
|
+
const result: Record<string, FilterOption[]> = {}
|
|
152
|
+
for (const col of props.columns) {
|
|
153
|
+
const search = localSearches.value[col.name]?.toLowerCase() || ''
|
|
154
|
+
const options = props.filtersOptions[col.name] || []
|
|
155
|
+
result[col.name] = options.filter(opt => opt.name.toLowerCase().includes(search))
|
|
156
|
+
}
|
|
157
|
+
return result
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
watch(
|
|
161
|
+
() => props.selectedRows,
|
|
162
|
+
val => {
|
|
163
|
+
selected.value = val
|
|
164
|
+
},
|
|
165
|
+
{ immediate: true },
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
watch(
|
|
169
|
+
selected,
|
|
170
|
+
val => {
|
|
171
|
+
emit('update:selectedRows', val)
|
|
172
|
+
},
|
|
173
|
+
{ deep: true },
|
|
174
|
+
)
|
|
175
|
+
</script>
|
|
179
176
|
<style scoped lang="scss">
|
|
180
177
|
.cursor-pointer {
|
|
181
178
|
cursor: pointer;
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import AppTable from './AppTable.vue'
|
|
3
|
-
import AppTablePagination from '../app-table/components/TablePagination.vue'
|
|
4
|
-
import AppTableSearch from '../app-table/components/TableSearch.vue'
|
|
5
|
-
import { defineProps, defineEmits } from 'vue'
|
|
6
|
-
|
|
7
|
-
const emit = defineEmits<{
|
|
8
|
-
'update:selectedRows': [rows: any[]]
|
|
9
|
-
}>()
|
|
10
|
-
const props = defineProps<{
|
|
11
|
-
search: string
|
|
12
|
-
loading: boolean
|
|
13
|
-
currentPage: number
|
|
14
|
-
totalPages: number
|
|
15
|
-
tableProps: any
|
|
16
|
-
tableEvents: any
|
|
17
|
-
onSearch: (val: string) => void
|
|
18
|
-
onPageChange: (page: number) => void
|
|
19
|
-
actionsSlot?: boolean
|
|
20
|
-
modalSlot?: boolean
|
|
21
|
-
selectedRows: any[]
|
|
22
|
-
}>()
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
1
|
<template>
|
|
26
2
|
<div class="table-layout">
|
|
27
3
|
<div class="table-controls">
|
|
@@ -55,7 +31,29 @@ const props = defineProps<{
|
|
|
55
31
|
<slot v-if="modalSlot" name="modal" />
|
|
56
32
|
</div>
|
|
57
33
|
</template>
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import AppTable from './AppTable.vue'
|
|
36
|
+
import AppTablePagination from '../app-table/components/TablePagination.vue'
|
|
37
|
+
import AppTableSearch from '../app-table/components/TableSearch.vue'
|
|
38
|
+
import { defineProps, defineEmits } from 'vue'
|
|
58
39
|
|
|
40
|
+
const props = defineProps<{
|
|
41
|
+
search: string
|
|
42
|
+
loading: boolean
|
|
43
|
+
currentPage: number
|
|
44
|
+
totalPages: number
|
|
45
|
+
tableProps: any
|
|
46
|
+
tableEvents: any
|
|
47
|
+
onSearch: (val: string) => void
|
|
48
|
+
onPageChange: (page: number) => void
|
|
49
|
+
actionsSlot?: boolean
|
|
50
|
+
modalSlot?: boolean
|
|
51
|
+
selectedRows: any[]
|
|
52
|
+
}>()
|
|
53
|
+
const emit = defineEmits<{
|
|
54
|
+
'update:selectedRows': [rows: any[]]
|
|
55
|
+
}>()
|
|
56
|
+
</script>
|
|
59
57
|
<style scoped lang="scss">
|
|
60
58
|
.table-layout {
|
|
61
59
|
height: calc(100vh - 100px);
|
|
@@ -1,154 +1,3 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
|
|
3
|
-
import AppModalSelect from '../components/ModalSelect.vue'
|
|
4
|
-
import { useQuasar } from 'quasar'
|
|
5
|
-
import { notificationSettings } from '@/utils/notification'
|
|
6
|
-
|
|
7
|
-
const $q = useQuasar()
|
|
8
|
-
|
|
9
|
-
type ModalMode = 'view' | 'edit' | 'create'
|
|
10
|
-
type FieldType = 'text' | 'select'
|
|
11
|
-
|
|
12
|
-
interface FieldOption {
|
|
13
|
-
label: string
|
|
14
|
-
value: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface FieldSchema {
|
|
18
|
-
key: string
|
|
19
|
-
label: string
|
|
20
|
-
type: FieldType
|
|
21
|
-
rules?: ((val: any) => boolean | string)[]
|
|
22
|
-
options?: FieldOption[]
|
|
23
|
-
placeholder?: string
|
|
24
|
-
onSearch?: (val: string) => void
|
|
25
|
-
onScroll?: () => void
|
|
26
|
-
loading?: boolean
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const props = defineProps<{
|
|
30
|
-
modelValue: boolean
|
|
31
|
-
title: string
|
|
32
|
-
mode: ModalMode
|
|
33
|
-
fields: FieldSchema[]
|
|
34
|
-
initialData?: Record<string, any>
|
|
35
|
-
}>()
|
|
36
|
-
|
|
37
|
-
const emit = defineEmits<{
|
|
38
|
-
(e: 'update:modelValue', val: boolean): void
|
|
39
|
-
(e: 'submit', data: Record<string, any>): void
|
|
40
|
-
(e: 'edit'): void
|
|
41
|
-
(e: 'delete'): void
|
|
42
|
-
}>()
|
|
43
|
-
|
|
44
|
-
const formData = ref<Record<string, any>>({})
|
|
45
|
-
const formRef = ref()
|
|
46
|
-
|
|
47
|
-
watch(
|
|
48
|
-
() => props.modelValue,
|
|
49
|
-
val => {
|
|
50
|
-
if (val) {
|
|
51
|
-
formData.value = { ...(props.initialData ?? {}) }
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
{ immediate: true },
|
|
55
|
-
)
|
|
56
|
-
function isEqual(a: any, b: any): boolean {
|
|
57
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
58
|
-
if (a.length !== b.length) return false
|
|
59
|
-
return a.every((item, i) => isEqual(item, b[i]))
|
|
60
|
-
}
|
|
61
|
-
if (typeof a === 'object' && typeof b === 'object') {
|
|
62
|
-
return JSON.stringify(a) === JSON.stringify(b)
|
|
63
|
-
}
|
|
64
|
-
return a === b
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function normalizeValue(val: any): any {
|
|
68
|
-
if (Array.isArray(val)) {
|
|
69
|
-
return val.map(v => (typeof v === 'object' && v !== null ? v.value ?? v : v))
|
|
70
|
-
}
|
|
71
|
-
return typeof val === 'object' && val !== null ? val.value ?? val : val
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function submit() {
|
|
75
|
-
formRef.value?.validate().then(ok => {
|
|
76
|
-
if (!ok) return
|
|
77
|
-
|
|
78
|
-
const changed: Record<string, any> = {}
|
|
79
|
-
|
|
80
|
-
for (const field of props.fields) {
|
|
81
|
-
const key = field.key
|
|
82
|
-
const current = formData.value[key]
|
|
83
|
-
const initial = props.initialData?.[key]
|
|
84
|
-
|
|
85
|
-
const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
|
|
86
|
-
const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
|
|
87
|
-
|
|
88
|
-
if (!isEqual(normalizedCurrent, normalizedInitial)) {
|
|
89
|
-
changed[key] = normalizedCurrent
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
emit('submit', changed)
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function handleClear(key: string) {
|
|
98
|
-
const field = props.fields.find(f => f.key === key)
|
|
99
|
-
formData.value[key] = field?.type === 'select' ? [] : ''
|
|
100
|
-
|
|
101
|
-
nextTick(() => {
|
|
102
|
-
formRef.value?.validate()
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
const isSubmitDisabled = computed(() => {
|
|
106
|
-
if (props.mode === 'view') return false
|
|
107
|
-
|
|
108
|
-
const hasEmptyRequired = props.fields.some(field => {
|
|
109
|
-
const isRequired = field.rules?.some(rule => rule('') !== true)
|
|
110
|
-
const val = formData.value[field.key]
|
|
111
|
-
const normalized = normalizeValue(val)
|
|
112
|
-
|
|
113
|
-
const empty =
|
|
114
|
-
normalized === null ||
|
|
115
|
-
normalized === undefined ||
|
|
116
|
-
(typeof normalized === 'string' && normalized.trim() === '') ||
|
|
117
|
-
(Array.isArray(normalized) && normalized.length === 0)
|
|
118
|
-
|
|
119
|
-
return isRequired && empty
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
if (hasEmptyRequired) return true
|
|
123
|
-
|
|
124
|
-
const hasChanges = props.fields.some(field => {
|
|
125
|
-
const key = field.key
|
|
126
|
-
const current = normalizeValue(formData.value[key])
|
|
127
|
-
const initial = normalizeValue(props.initialData?.[key])
|
|
128
|
-
return !isEqual(current, initial)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
return !hasChanges
|
|
132
|
-
})
|
|
133
|
-
function uuidv4() {
|
|
134
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
135
|
-
const r = (Math.random() * 16) | 0
|
|
136
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
137
|
-
return v.toString(16)
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function generateUuid() {
|
|
142
|
-
formData.value.uuid = uuidv4()
|
|
143
|
-
}
|
|
144
|
-
function copyToClipboard(text: string) {
|
|
145
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
146
|
-
$q.notify(notificationSettings('success', 'UUID скопирован'))
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
const filteredOptions = ref<Record<string, FieldOption[]>>({})
|
|
150
|
-
</script>
|
|
151
|
-
|
|
152
1
|
<template>
|
|
153
2
|
<q-dialog :model-value="modelValue" @update:model-value="val => emit('update:modelValue', val)">
|
|
154
3
|
<q-card style="min-width: 700px">
|
|
@@ -243,6 +92,129 @@ const filteredOptions = ref<Record<string, FieldOption[]>>({})
|
|
|
243
92
|
</q-card>
|
|
244
93
|
</q-dialog>
|
|
245
94
|
</template>
|
|
95
|
+
|
|
96
|
+
<script setup lang="ts">
|
|
97
|
+
import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
|
|
98
|
+
import { useQuasar } from 'quasar'
|
|
99
|
+
import AppModalSelect from '../components/ModalSelect.vue'
|
|
100
|
+
import { notificationSettings } from '@/utils/notification'
|
|
101
|
+
import { isEqual, normalizeValue, uuidv4 } from '@/utils/helpers'
|
|
102
|
+
|
|
103
|
+
type ModalMode = 'view' | 'edit' | 'create'
|
|
104
|
+
type FieldType = 'text' | 'select'
|
|
105
|
+
|
|
106
|
+
interface FieldOption {
|
|
107
|
+
label: string
|
|
108
|
+
value: string
|
|
109
|
+
}
|
|
110
|
+
interface FieldSchema {
|
|
111
|
+
key: string
|
|
112
|
+
label: string
|
|
113
|
+
type: FieldType
|
|
114
|
+
rules?: ((val: any) => boolean | string)[]
|
|
115
|
+
options?: FieldOption[]
|
|
116
|
+
placeholder?: string
|
|
117
|
+
onSearch?: (val: string) => void
|
|
118
|
+
onScroll?: () => void
|
|
119
|
+
loading?: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const props = defineProps<{
|
|
123
|
+
modelValue: boolean
|
|
124
|
+
title: string
|
|
125
|
+
mode: ModalMode
|
|
126
|
+
fields: FieldSchema[]
|
|
127
|
+
initialData?: Record<string, any>
|
|
128
|
+
}>()
|
|
129
|
+
const emit = defineEmits<{
|
|
130
|
+
(e: 'update:modelValue', val: boolean): void
|
|
131
|
+
(e: 'submit', data: Record<string, any>): void
|
|
132
|
+
(e: 'edit'): void
|
|
133
|
+
(e: 'delete'): void
|
|
134
|
+
}>()
|
|
135
|
+
|
|
136
|
+
const $q = useQuasar()
|
|
137
|
+
const formRef = ref()
|
|
138
|
+
const formData = ref<Record<string, any>>({})
|
|
139
|
+
const filteredOptions = ref<Record<string, FieldOption[]>>({})
|
|
140
|
+
const isSubmitDisabled = computed(() => {
|
|
141
|
+
if (props.mode === 'view') return false
|
|
142
|
+
|
|
143
|
+
const hasEmptyRequired = props.fields.some(field => {
|
|
144
|
+
const isRequired = field.rules?.some(rule => rule('') !== true)
|
|
145
|
+
const val = formData.value[field.key]
|
|
146
|
+
const normalized = normalizeValue(val)
|
|
147
|
+
|
|
148
|
+
const empty =
|
|
149
|
+
normalized === null ||
|
|
150
|
+
normalized === undefined ||
|
|
151
|
+
(typeof normalized === 'string' && normalized.trim() === '') ||
|
|
152
|
+
(Array.isArray(normalized) && normalized.length === 0)
|
|
153
|
+
|
|
154
|
+
return isRequired && empty
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (hasEmptyRequired) return true
|
|
158
|
+
|
|
159
|
+
const hasChanges = props.fields.some(field => {
|
|
160
|
+
const key = field.key
|
|
161
|
+
const current = normalizeValue(formData.value[key])
|
|
162
|
+
const initial = normalizeValue(props.initialData?.[key])
|
|
163
|
+
return !isEqual(current, initial)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return !hasChanges
|
|
167
|
+
})
|
|
168
|
+
function submit() {
|
|
169
|
+
formRef.value?.validate().then(ok => {
|
|
170
|
+
if (!ok) return
|
|
171
|
+
|
|
172
|
+
const changed: Record<string, any> = {}
|
|
173
|
+
|
|
174
|
+
for (const field of props.fields) {
|
|
175
|
+
const key = field.key
|
|
176
|
+
const current = formData.value[key]
|
|
177
|
+
const initial = props.initialData?.[key]
|
|
178
|
+
|
|
179
|
+
const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
|
|
180
|
+
const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
|
|
181
|
+
|
|
182
|
+
if (!isEqual(normalizedCurrent, normalizedInitial)) {
|
|
183
|
+
changed[key] = normalizedCurrent
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
emit('submit', changed)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
function handleClear(key: string) {
|
|
191
|
+
const field = props.fields.find(f => f.key === key)
|
|
192
|
+
formData.value[key] = field?.type === 'select' ? [] : ''
|
|
193
|
+
|
|
194
|
+
nextTick(() => {
|
|
195
|
+
formRef.value?.validate()
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
function generateUuid() {
|
|
199
|
+
formData.value.uuid = uuidv4()
|
|
200
|
+
}
|
|
201
|
+
function copyToClipboard(text: string) {
|
|
202
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
203
|
+
$q.notify(notificationSettings('success', 'UUID скопирован'))
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
watch(
|
|
208
|
+
() => props.modelValue,
|
|
209
|
+
val => {
|
|
210
|
+
if (val) {
|
|
211
|
+
formData.value = { ...(props.initialData ?? {}) }
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{ immediate: true },
|
|
215
|
+
)
|
|
216
|
+
</script>
|
|
217
|
+
|
|
246
218
|
<style lang="scss">
|
|
247
219
|
.custom-select-menu {
|
|
248
220
|
max-height: 250px !important;
|