shared-ritm 1.2.46 → 1.2.48

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.
Files changed (91) hide show
  1. package/README.md +103 -103
  2. package/dist/index.css +1 -1
  3. package/dist/shared-ritm.es.js +199855 -72176
  4. package/dist/shared-ritm.umd.js +97 -42
  5. package/dist/types/api/services/ControlsService.d.ts +1 -0
  6. package/dist/types/api/types/Api_Controls.d.ts +12 -2
  7. package/dist/types/index.d.ts +2 -1
  8. package/package.json +1 -1
  9. package/src/api/services/ControlsService.ts +40 -36
  10. package/src/api/services/FileService.ts +15 -15
  11. package/src/api/services/GanttService.ts +17 -17
  12. package/src/api/services/MetricsService.ts +109 -109
  13. package/src/api/services/RepairsService.ts +100 -100
  14. package/src/api/settings/ApiService.ts +128 -128
  15. package/src/api/types/Api_Controls.ts +12 -2
  16. package/src/api/types/Api_Files.ts +1 -1
  17. package/src/api/types/Api_Projects.ts +55 -55
  18. package/src/api/types/Api_Repairs.ts +93 -93
  19. package/src/common/app-button/AppButton.vue +173 -173
  20. package/src/common/app-checkbox/AppCheckbox.vue +26 -26
  21. package/src/common/app-dialogs/AppConfirmDialog.vue +99 -99
  22. package/src/common/app-dropdown/AppDropdown.vue +31 -31
  23. package/src/common/app-input/AppInput.vue +147 -147
  24. package/src/common/app-loader/index.vue +43 -43
  25. package/src/common/app-page-layout/AppPageLayout.vue +122 -122
  26. package/src/common/app-select/AppSelect.vue +157 -157
  27. package/src/common/app-sidebar/AppSidebar.vue +168 -168
  28. package/src/common/app-sidebar/components/SidebarMenu.vue +37 -37
  29. package/src/common/app-sidebar/components/SidebarMenuItem.vue +148 -148
  30. package/src/common/app-table/AppTable.vue +211 -211
  31. package/src/common/app-table/AppTableLayout.vue +93 -93
  32. package/src/common/app-table/components/ModalSelect.vue +215 -0
  33. package/src/common/app-table/components/TableModal.vue +26 -53
  34. package/src/common/app-table/components/TablePagination.vue +151 -151
  35. package/src/common/app-table/components/TableSearch.vue +78 -78
  36. package/src/common/app-table/controllers/useBaseTable.ts +43 -43
  37. package/src/common/app-table/controllers/useColumnSelector.ts +38 -38
  38. package/src/common/app-table/controllers/useTableModel.ts +93 -93
  39. package/src/common/app-toggle/AppToggle.vue +23 -23
  40. package/src/common/app-wrapper/AppWrapper.vue +28 -28
  41. package/src/global.d.ts +1 -1
  42. package/src/icons/components/arrow-down-icon.vue +25 -25
  43. package/src/icons/components/arrow-frame-icon.vue +19 -19
  44. package/src/icons/components/arrow-square.vue +22 -22
  45. package/src/icons/components/table-filter-icon.vue +30 -30
  46. package/src/icons/dialogs/RemoveIcon.vue +12 -12
  47. package/src/icons/dialogs/SafetyIcon.vue +12 -12
  48. package/src/icons/header/flashIcon.vue +24 -24
  49. package/src/icons/header/searchStatusIcon.vue +24 -24
  50. package/src/icons/header/smallCapsIcon.vue +34 -34
  51. package/src/icons/sidebar/assign-module-icon.vue +36 -36
  52. package/src/icons/sidebar/instrument-history-icon.vue +32 -32
  53. package/src/icons/sidebar/instrument-order-icon.vue +38 -38
  54. package/src/icons/sidebar/instrument-work-zone-icon.vue +18 -18
  55. package/src/icons/sidebar/instruments-icon.vue +45 -45
  56. package/src/icons/sidebar/logo-icon.vue +15 -15
  57. package/src/icons/sidebar/logout-icon.vue +13 -13
  58. package/src/icons/sidebar/modules-icon.vue +16 -16
  59. package/src/icons/sidebar/notifications-icon.vue +24 -24
  60. package/src/icons/sidebar/order-icon.vue +44 -44
  61. package/src/icons/sidebar/pass-icon.vue +38 -38
  62. package/src/icons/sidebar/positions-icon.vue +42 -42
  63. package/src/icons/sidebar/preorder-icon.vue +19 -19
  64. package/src/icons/sidebar/projects-icon.vue +31 -31
  65. package/src/icons/sidebar/repair-object-icon.vue +18 -18
  66. package/src/icons/sidebar/repairs-icon.vue +20 -20
  67. package/src/icons/sidebar/roles-icon.vue +26 -26
  68. package/src/icons/sidebar/status-history-icon.vue +24 -24
  69. package/src/icons/sidebar/tasks-icon.vue +28 -28
  70. package/src/icons/sidebar/tasks_tasks-icon.vue +39 -39
  71. package/src/icons/sidebar/tasks_today-icon.vue +27 -27
  72. package/src/icons/sidebar/teams-icon.vue +32 -32
  73. package/src/icons/sidebar/user-icon.vue +18 -18
  74. package/src/icons/sidebar/users-icon.vue +46 -46
  75. package/src/icons/sidebar/videosources-icon.vue +19 -19
  76. package/src/icons/sidebar/videowall-icon.vue +13 -13
  77. package/src/icons/sidebar/videozones-icon.vue +21 -21
  78. package/src/icons/sidebar/warehouses-icon.vue +43 -43
  79. package/src/icons/sidebar/workshop-icon.vue +100 -100
  80. package/src/icons/sidebar/workzones-icon.vue +22 -22
  81. package/src/icons/task/attention-icon.vue +13 -13
  82. package/src/icons/task/clock-icon.vue +10 -10
  83. package/src/icons/task/delete-icon.vue +10 -10
  84. package/src/icons/task/fire-icon.vue +16 -16
  85. package/src/index.ts +2 -0
  86. package/src/quasar-user-options.ts +17 -17
  87. package/src/router/index.ts +10 -10
  88. package/src/shared/styles/general.css +96 -96
  89. package/src/shims-vue.d.ts +5 -5
  90. package/src/utils/confirm.ts +12 -12
  91. package/src/utils/notification.ts +9 -9
@@ -0,0 +1,215 @@
1
+ <template>
2
+ <label class="field-label">
3
+ {{ label }}
4
+ <span v-if="rules?.length && isShowRequired" class="required">*</span>
5
+ </label>
6
+ <q-select
7
+ ref="select"
8
+ v-model="selected"
9
+ :options="filteredOptions"
10
+ :disable="isDisabled"
11
+ :multiple="multiple"
12
+ :popup-content-class="'custom-select-menu'"
13
+ :hide-selected="!showChip"
14
+ :placeholder="placeholder"
15
+ :loading="loading"
16
+ :option-value="optionValue || 'value'"
17
+ :option-label="optionLabel || 'label'"
18
+ emit-value
19
+ filled
20
+ map-options
21
+ stack-label
22
+ use-input
23
+ use-chips
24
+ input-debounce="100"
25
+ autocomplete=""
26
+ :rules="rules"
27
+ @virtual-scroll="onScroll"
28
+ @filter="filterFn"
29
+ >
30
+ <template v-if="multiple || showChip" #selected-item="scope">
31
+ <q-chip
32
+ v-if="scope.opt"
33
+ removable
34
+ :tabindex="scope.tabindex"
35
+ :style="{ backgroundColor: chipColor }"
36
+ icon-remove="close"
37
+ text-color="secondary"
38
+ @remove="scope.removeAtIndex(scope.index)"
39
+ >
40
+ {{ scope.opt[optionLabel || 'label'] }}
41
+ </q-chip>
42
+ </template>
43
+
44
+ <template #append>
45
+ <q-icon
46
+ v-if="!isDisabled && selected && selected.length"
47
+ name="close"
48
+ class="cursor-pointer clear-input"
49
+ @click.stop="handleClear"
50
+ />
51
+ </template>
52
+
53
+ <template #no-option>
54
+ <q-item>{{ emptyText }}</q-item>
55
+ </template>
56
+
57
+ <template #option="scope">
58
+ <q-item v-if="scope.opt.__loading" class="q-py-md q-ml-md">
59
+ <q-spinner-dots size="24px" color="primary" />
60
+ </q-item>
61
+ <q-item v-else v-bind="scope.itemProps">
62
+ <q-item-section>{{ scope.opt[optionLabel || 'label'] }}</q-item-section>
63
+ </q-item>
64
+ </template>
65
+ </q-select>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { computed, defineEmits, defineProps, ref, Ref } from 'vue'
70
+
71
+ type Option = Record<string, any>
72
+
73
+ interface AppQSelectProps {
74
+ modelValue: any
75
+ options: Option[]
76
+ placeholder: string | undefined
77
+ emptyText: string
78
+ optionLabel?: string
79
+ optionValue?: string
80
+ label?: string
81
+ multiple?: boolean
82
+ loading?: boolean
83
+ isShowRequired?: boolean
84
+ isDisabled?: boolean
85
+ isSearch?: boolean
86
+ showChip?: boolean
87
+ chipColor?: string
88
+ height?: string
89
+ borderColor?: string
90
+ borderRadius?: string
91
+ menuWidth?: string
92
+ rules?: ((val: any) => boolean | string)[]
93
+ }
94
+
95
+ const props = defineProps<AppQSelectProps>()
96
+ const emit = defineEmits(['update:modelValue', 'update:scroll', 'update:search', 'clear'])
97
+
98
+ const select = ref({})
99
+ const lcText: Ref<string> = ref('')
100
+
101
+ const selected = computed({
102
+ get() {
103
+ return props.modelValue
104
+ },
105
+ set(value) {
106
+ emit('update:modelValue', value)
107
+ },
108
+ })
109
+
110
+ function handleClear() {
111
+ selected.value = props.multiple ? [] : null
112
+ lcText.value = ''
113
+ emit('update:modelValue', selected.value)
114
+ emit('clear')
115
+ }
116
+
117
+ const filteredOptions = computed(() => {
118
+ const baseOptions = props.options.filter(x => x[props?.optionLabel || 'label'].toLowerCase().includes(lcText.value))
119
+
120
+ if (props.loading) {
121
+ return [
122
+ ...baseOptions,
123
+ {
124
+ __loading: true,
125
+ label: '__loading__',
126
+ value: '__loading__',
127
+ },
128
+ ]
129
+ }
130
+
131
+ return baseOptions
132
+ })
133
+
134
+ function filterFn(val: string, update: (cb: () => void) => void) {
135
+ debouncedFilter(val, update)
136
+ }
137
+
138
+ const debouncedFilter = debounce((val: string, update: (cb: () => void) => void) => {
139
+ emit('update:search', val)
140
+ update(() => {
141
+ lcText.value = val.toLowerCase()
142
+ })
143
+ }, 500)
144
+
145
+ function onScroll({ to, ref: qSelectRef }) {
146
+ const totalOptions = qSelectRef.options.length
147
+ if (to >= totalOptions - 1 && !lcText.value) {
148
+ emit('update:scroll')
149
+ }
150
+ }
151
+
152
+ function debounce<T>(fn: T, ms) {
153
+ let timeoutId
154
+
155
+ return function (...args) {
156
+ clearTimeout(timeoutId)
157
+
158
+ return new Promise(resolve => {
159
+ timeoutId = setTimeout(() => {
160
+ resolve(fn(...args))
161
+ }, ms)
162
+ })
163
+ }
164
+ }
165
+ </script>
166
+
167
+ <style lang="scss" scoped>
168
+ .field-label {
169
+ font-size: 14px;
170
+ font-weight: 700;
171
+ color: #7d8592;
172
+ }
173
+ .required {
174
+ color: #f65160;
175
+ font-weight: bold;
176
+ }
177
+
178
+ ::v-deep(.q-placeholder) {
179
+ color: #7d8592;
180
+ }
181
+ ::v-deep(.q-field__control) {
182
+ border-radius: 8px;
183
+ border: 1px solid #d8e0f0;
184
+ background: #fff;
185
+ box-shadow: 0px 1px 2px 0px rgba(184, 200, 224, 0.22);
186
+ }
187
+ ::v-deep(.q-field--filled.q-field--highlighted .q-field__control:before),
188
+ ::v-deep(.q-field--filled .q-field__control:before) {
189
+ background: #fff !important;
190
+ border: none;
191
+ }
192
+ ::v-deep(.q-field--with-bottom) {
193
+ padding-bottom: 0;
194
+ }
195
+ ::v-deep(.q-field__bottom) {
196
+ padding: 0;
197
+ }
198
+ .clear-input {
199
+ color: #d8e0f0;
200
+ }
201
+ ::v-deep(.q-chip) {
202
+ border-radius: 4px;
203
+ background: #e9eff9;
204
+ color: #1d425d;
205
+ font-family: NunitoSansFont, sans-serif;
206
+ font-size: 14px;
207
+ height: 30px;
208
+ line-height: 30px;
209
+ padding: 0 15px;
210
+ .q-chip__icon {
211
+ color: #3f8cff;
212
+ margin-left: 5px;
213
+ }
214
+ }
215
+ </style>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
3
+ import { AppModalSelect } from 'shared-ritm'
3
4
 
4
5
  type ModalMode = 'view' | 'edit' | 'create'
5
6
  type FieldType = 'text' | 'select'
@@ -16,6 +17,9 @@ interface FieldSchema {
16
17
  rules?: ((val: any) => boolean | string)[]
17
18
  options?: FieldOption[]
18
19
  placeholder?: string
20
+ onSearch?: (val: string) => void
21
+ onScroll?: () => void
22
+ loading?: boolean
19
23
  }
20
24
 
21
25
  const props = defineProps<{
@@ -134,17 +138,6 @@ function generateUuid() {
134
138
  formData.value.uuid = uuidv4()
135
139
  }
136
140
  const filteredOptions = ref<Record<string, FieldOption[]>>({})
137
-
138
- function onFilter(key: string) {
139
- return (val: string, update: (cb: () => void) => void) => {
140
- update(() => {
141
- const field = props.fields.find(f => f.key === key)
142
- if (!field?.options) return
143
- const lower = val.toLowerCase()
144
- filteredOptions.value[key] = field.options.filter(opt => opt.label.toLowerCase().includes(lower))
145
- })
146
- }
147
- }
148
141
  </script>
149
142
 
150
143
  <template>
@@ -157,7 +150,7 @@ function onFilter(key: string) {
157
150
  <q-card-section>
158
151
  <q-form ref="formRef" @submit.prevent="submit">
159
152
  <div v-for="field in fields" :key="field.key" class="field-wrapper">
160
- <label class="field-label">
153
+ <label v-if="field.type === 'text'" class="field-label">
161
154
  {{ field.label }}
162
155
  <span v-if="field.rules?.length && mode !== 'view'" class="required">*</span>
163
156
  </label>
@@ -189,38 +182,31 @@ function onFilter(key: string) {
189
182
  </template>
190
183
  </q-input>
191
184
 
192
- <q-select
185
+ <app-modal-select
193
186
  v-else-if="field.type === 'select'"
194
187
  v-model="formData[field.key]"
195
188
  :options="filteredOptions[field.key] || field.options"
196
189
  :rules="field.rules"
197
- :readonly="mode === 'view'"
198
190
  :placeholder="mode === 'view' ? '' : field.placeholder"
199
- filled
200
- multiple
201
- use-input
202
- input-debounce="0"
203
- emit-value
204
- map-options
205
- use-chips
206
- stack-label
207
- popup-content-class="custom-select-menu"
208
- @filter="(val, update) => onFilter(field.key)(val, update)"
209
- >
210
- <template v-if="mode !== 'view'" #append>
211
- <q-icon
212
- v-if="formData[field.key]?.length"
213
- name="close"
214
- class="cursor-pointer clear-input"
215
- @click.stop="
216
- () => {
217
- handleClear(field.key)
218
- nextTick(() => formRef.value?.validate())
219
- }
220
- "
221
- />
222
- </template>
223
- </q-select>
191
+ :multiple="true"
192
+ :show-chip="true"
193
+ :is-disabled="mode === 'view'"
194
+ :loading="field.loading"
195
+ :label="field.label"
196
+ :is-show-required="mode !== 'view'"
197
+ empty-text="Ничего не найдено"
198
+ option-label="label"
199
+ option-value="value"
200
+ chip-color="#e9eff9"
201
+ @update:search="val => field.onSearch?.(val)"
202
+ @update:scroll="() => field.onScroll?.()"
203
+ @clear="
204
+ () => {
205
+ handleClear(field.key)
206
+ nextTick(() => formRef.value?.validate())
207
+ }
208
+ "
209
+ />
224
210
  </div>
225
211
  </q-form>
226
212
  </q-card-section>
@@ -318,20 +304,7 @@ function onFilter(key: string) {
318
304
  .clear-input {
319
305
  color: #d8e0f0;
320
306
  }
321
- ::v-deep(.q-chip) {
322
- border-radius: 4px;
323
- background: #e9eff9;
324
- color: #1d425d;
325
- font-family: NunitoSansFont, sans-serif;
326
- font-size: 14px;
327
- height: 30px;
328
- line-height: 30px;
329
- padding: 0 10px;
330
- .q-chip__icon {
331
- color: #3f8cff;
332
- margin-left: 5px;
333
- }
334
- }
307
+
335
308
  .required {
336
309
  color: #f65160;
337
310
  font-weight: bold;
@@ -1,151 +1,151 @@
1
- <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import { defineProps, defineEmits } from 'vue'
4
-
5
- const props = defineProps<{
6
- modelValue: number
7
- totalPages: number
8
- }>()
9
-
10
- const emit = defineEmits<{
11
- (e: 'update:modelValue', value: number): void
12
- (e: 'page-change', value: number): void
13
- }>()
14
-
15
- const currentPage = computed({
16
- get: () => props.modelValue,
17
- set: (val: number) => {
18
- emit('update:modelValue', val)
19
- emit('page-change', val)
20
- },
21
- })
22
-
23
- const pageArray = computed(() => {
24
- const maxPagesToShow = 5
25
- let startPage = 1
26
- let endPage = props.totalPages
27
- const pages: (number | string)[] = []
28
-
29
- if (props.totalPages <= maxPagesToShow) {
30
- for (let i = 1; i <= props.totalPages; i++) pages.push(i)
31
- } else {
32
- if (props.modelValue <= 3) {
33
- startPage = 1
34
- endPage = maxPagesToShow - 1
35
- } else if (props.modelValue + 2 >= props.totalPages) {
36
- startPage = props.totalPages - (maxPagesToShow - 2)
37
- endPage = props.totalPages
38
- } else {
39
- startPage = props.modelValue - 2
40
- endPage = props.modelValue + 2
41
- }
42
-
43
- for (let i = startPage; i <= endPage; i++) pages.push(i)
44
-
45
- if (startPage > 1) {
46
- pages.unshift('...')
47
- pages.unshift(1)
48
- }
49
- if (endPage < props.totalPages) {
50
- pages.push('...')
51
- pages.push(props.totalPages)
52
- }
53
- }
54
-
55
- return pages
56
- })
57
-
58
- function changePage(page: number | string) {
59
- if (typeof page === 'number' && page !== props.modelValue) {
60
- currentPage.value = page
61
- }
62
- }
63
-
64
- function prevPage() {
65
- if (props.modelValue > 1) currentPage.value = props.modelValue - 1
66
- }
67
-
68
- function nextPage() {
69
- if (props.modelValue < props.totalPages) currentPage.value = props.modelValue + 1
70
- }
71
- </script>
72
-
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
- <style scoped lang="scss">
99
- .pages {
100
- display: flex;
101
- gap: 20px;
102
- }
103
- .table-pagination {
104
- display: flex;
105
- justify-content: start;
106
- align-items: center;
107
- gap: 20px;
108
- flex-wrap: wrap;
109
- margin-top: 17px;
110
- margin-bottom: 15px;
111
- .arrow-button,
112
- .page-button {
113
- width: 34px;
114
- height: 34px;
115
- border-radius: 4px;
116
- background: white;
117
- border: none;
118
- cursor: pointer;
119
- font-weight: 500;
120
- color: #3f8cff;
121
- font-size: 16px;
122
- transition: all 0.2s ease;
123
- }
124
-
125
- .page-button.selected {
126
- background: #3f8cff;
127
- color: white;
128
- cursor: not-allowed;
129
- }
130
-
131
- .page-button.ellipsis {
132
- cursor: default;
133
- color: #aab2c8;
134
- font-weight: 400;
135
- }
136
-
137
- .arrow-button:disabled {
138
- cursor: not-allowed;
139
- background: white;
140
- }
141
-
142
- @media (max-width: 768px) {
143
- .arrow-button,
144
- .page-button {
145
- width: 32px;
146
- height: 32px;
147
- font-size: 14px;
148
- }
149
- }
150
- }
151
- </style>
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { defineProps, defineEmits } from 'vue'
4
+
5
+ const props = defineProps<{
6
+ modelValue: number
7
+ totalPages: number
8
+ }>()
9
+
10
+ const emit = defineEmits<{
11
+ (e: 'update:modelValue', value: number): void
12
+ (e: 'page-change', value: number): void
13
+ }>()
14
+
15
+ const currentPage = computed({
16
+ get: () => props.modelValue,
17
+ set: (val: number) => {
18
+ emit('update:modelValue', val)
19
+ emit('page-change', val)
20
+ },
21
+ })
22
+
23
+ const pageArray = computed(() => {
24
+ const maxPagesToShow = 5
25
+ let startPage = 1
26
+ let endPage = props.totalPages
27
+ const pages: (number | string)[] = []
28
+
29
+ if (props.totalPages <= maxPagesToShow) {
30
+ for (let i = 1; i <= props.totalPages; i++) pages.push(i)
31
+ } else {
32
+ if (props.modelValue <= 3) {
33
+ startPage = 1
34
+ endPage = maxPagesToShow - 1
35
+ } else if (props.modelValue + 2 >= props.totalPages) {
36
+ startPage = props.totalPages - (maxPagesToShow - 2)
37
+ endPage = props.totalPages
38
+ } else {
39
+ startPage = props.modelValue - 2
40
+ endPage = props.modelValue + 2
41
+ }
42
+
43
+ for (let i = startPage; i <= endPage; i++) pages.push(i)
44
+
45
+ if (startPage > 1) {
46
+ pages.unshift('...')
47
+ pages.unshift(1)
48
+ }
49
+ if (endPage < props.totalPages) {
50
+ pages.push('...')
51
+ pages.push(props.totalPages)
52
+ }
53
+ }
54
+
55
+ return pages
56
+ })
57
+
58
+ function changePage(page: number | string) {
59
+ if (typeof page === 'number' && page !== props.modelValue) {
60
+ currentPage.value = page
61
+ }
62
+ }
63
+
64
+ function prevPage() {
65
+ if (props.modelValue > 1) currentPage.value = props.modelValue - 1
66
+ }
67
+
68
+ function nextPage() {
69
+ if (props.modelValue < props.totalPages) currentPage.value = props.modelValue + 1
70
+ }
71
+ </script>
72
+
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
+ <style scoped lang="scss">
99
+ .pages {
100
+ display: flex;
101
+ gap: 20px;
102
+ }
103
+ .table-pagination {
104
+ display: flex;
105
+ justify-content: start;
106
+ align-items: center;
107
+ gap: 20px;
108
+ flex-wrap: wrap;
109
+ margin-top: 17px;
110
+ margin-bottom: 15px;
111
+ .arrow-button,
112
+ .page-button {
113
+ width: 34px;
114
+ height: 34px;
115
+ border-radius: 4px;
116
+ background: white;
117
+ border: none;
118
+ cursor: pointer;
119
+ font-weight: 500;
120
+ color: #3f8cff;
121
+ font-size: 16px;
122
+ transition: all 0.2s ease;
123
+ }
124
+
125
+ .page-button.selected {
126
+ background: #3f8cff;
127
+ color: white;
128
+ cursor: not-allowed;
129
+ }
130
+
131
+ .page-button.ellipsis {
132
+ cursor: default;
133
+ color: #aab2c8;
134
+ font-weight: 400;
135
+ }
136
+
137
+ .arrow-button:disabled {
138
+ cursor: not-allowed;
139
+ background: white;
140
+ }
141
+
142
+ @media (max-width: 768px) {
143
+ .arrow-button,
144
+ .page-button {
145
+ width: 32px;
146
+ height: 32px;
147
+ font-size: 14px;
148
+ }
149
+ }
150
+ }
151
+ </style>