shared-ritm 1.2.56 → 1.2.58
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 +3881 -3883
- package/dist/shared-ritm.umd.js +7 -7
- package/dist/types/utils/helpers.d.ts +17 -0
- package/package.json +1 -1
- package/src/common/app-input/AppInput.vue +1 -0
- 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 +24 -25
- package/src/common/app-table/components/TableSearch.vue +12 -14
- package/src/utils/helpers.ts +39 -0
|
@@ -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,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;
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="table-pagination">
|
|
3
|
+
<button class="arrow-button" :disabled="modelValue <= 1" @click="prevPage">
|
|
4
|
+
<q-icon name="mdi-chevron-left" :color="modelValue <= 1 ? 'grey-4' : 'primary'" />
|
|
5
|
+
</button>
|
|
6
|
+
|
|
7
|
+
<div class="pages">
|
|
8
|
+
<button
|
|
9
|
+
v-for="page in pageArray"
|
|
10
|
+
:key="page"
|
|
11
|
+
class="page-button"
|
|
12
|
+
:class="{ selected: page === modelValue, ellipsis: page === '...' }"
|
|
13
|
+
:disabled="page === modelValue || page === '...'"
|
|
14
|
+
@click="changePage(page)"
|
|
15
|
+
>
|
|
16
|
+
{{ page }}
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<button class="arrow-button" :disabled="modelValue >= totalPages" @click="nextPage">
|
|
21
|
+
<q-icon name="mdi-chevron-right" :color="modelValue >= totalPages ? 'grey-4' : 'primary'" />
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
1
25
|
<script setup lang="ts">
|
|
2
26
|
import { computed } from 'vue'
|
|
3
27
|
import { defineProps, defineEmits } from 'vue'
|
|
@@ -70,31 +94,6 @@ function nextPage() {
|
|
|
70
94
|
}
|
|
71
95
|
</script>
|
|
72
96
|
|
|
73
|
-
<template>
|
|
74
|
-
<div class="table-pagination">
|
|
75
|
-
<button class="arrow-button" :disabled="modelValue <= 1" @click="prevPage">
|
|
76
|
-
<q-icon name="mdi-chevron-left" :color="modelValue <= 1 ? 'grey-4' : 'primary'" />
|
|
77
|
-
</button>
|
|
78
|
-
|
|
79
|
-
<div class="pages">
|
|
80
|
-
<button
|
|
81
|
-
v-for="page in pageArray"
|
|
82
|
-
:key="page"
|
|
83
|
-
class="page-button"
|
|
84
|
-
:class="{ selected: page === modelValue, ellipsis: page === '...' }"
|
|
85
|
-
:disabled="page === modelValue || page === '...'"
|
|
86
|
-
@click="changePage(page)"
|
|
87
|
-
>
|
|
88
|
-
{{ page }}
|
|
89
|
-
</button>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<button class="arrow-button" :disabled="modelValue >= totalPages" @click="nextPage">
|
|
93
|
-
<q-icon name="mdi-chevron-right" :color="modelValue >= totalPages ? 'grey-4' : 'primary'" />
|
|
94
|
-
</button>
|
|
95
|
-
</div>
|
|
96
|
-
</template>
|
|
97
|
-
|
|
98
97
|
<style scoped lang="scss">
|
|
99
98
|
.pages {
|
|
100
99
|
display: flex;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-input v-model="input" :placeholder="placeholder" dense outlined>
|
|
3
|
+
<template v-if="input" #append>
|
|
4
|
+
<q-icon name="✕" class="cursor-pointer" @click.stop="clearInput" />
|
|
5
|
+
</template>
|
|
6
|
+
</q-input>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
1
9
|
<script setup lang="ts">
|
|
2
10
|
import { defineProps, defineEmits, ref, watch } from 'vue'
|
|
3
11
|
|
|
@@ -13,7 +21,10 @@ const emit = defineEmits<{
|
|
|
13
21
|
|
|
14
22
|
const input = ref(props.modelValue)
|
|
15
23
|
let debounceTimer: ReturnType<typeof setTimeout>
|
|
16
|
-
|
|
24
|
+
const clearInput = () => {
|
|
25
|
+
input.value = ''
|
|
26
|
+
emit('search', '')
|
|
27
|
+
}
|
|
17
28
|
watch(
|
|
18
29
|
() => props.modelValue,
|
|
19
30
|
val => {
|
|
@@ -28,21 +39,8 @@ watch(input, val => {
|
|
|
28
39
|
emit('search', val)
|
|
29
40
|
}, 400)
|
|
30
41
|
})
|
|
31
|
-
|
|
32
|
-
const clearInput = () => {
|
|
33
|
-
input.value = ''
|
|
34
|
-
emit('search', '')
|
|
35
|
-
}
|
|
36
42
|
</script>
|
|
37
43
|
|
|
38
|
-
<template>
|
|
39
|
-
<q-input v-model="input" :placeholder="placeholder" dense outlined>
|
|
40
|
-
<template v-if="input" #append>
|
|
41
|
-
<q-icon name="✕" class="cursor-pointer" @click.stop="clearInput" />
|
|
42
|
-
</template>
|
|
43
|
-
</q-input>
|
|
44
|
-
</template>
|
|
45
|
-
|
|
46
44
|
<style scoped lang="scss">
|
|
47
45
|
.q-field {
|
|
48
46
|
background: #fff;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сравнивает два значения на глубокое равенство.
|
|
3
|
+
* Поддерживает массивы, объекты и примитивы.
|
|
4
|
+
*/
|
|
5
|
+
export function isEqual(a: any, b: any): boolean {
|
|
6
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
7
|
+
if (a.length !== b.length) return false
|
|
8
|
+
return a.every((item, i) => isEqual(item, b[i]))
|
|
9
|
+
}
|
|
10
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
11
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
12
|
+
}
|
|
13
|
+
return a === b
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Нормализует значение:
|
|
18
|
+
* - Если передан массив объектов, возвращает массив `.value` или сам объект.
|
|
19
|
+
* - Если передан объект, возвращает `.value` или сам объект.
|
|
20
|
+
* - Если примитив — возвращает без изменений.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeValue(val: any): any {
|
|
23
|
+
if (Array.isArray(val)) {
|
|
24
|
+
return val.map(v => (typeof v === 'object' && v !== null ? v.value ?? v : v))
|
|
25
|
+
}
|
|
26
|
+
return typeof val === 'object' && val !== null ? val.value ?? val : val
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Генерирует UUID v4.
|
|
31
|
+
* Используется для идентификаторов временных сущностей.
|
|
32
|
+
*/
|
|
33
|
+
export function uuidv4(): string {
|
|
34
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
35
|
+
const r = (Math.random() * 16) | 0
|
|
36
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
37
|
+
return v.toString(16)
|
|
38
|
+
})
|
|
39
|
+
}
|