@7365admin1/layer-common 1.11.44 → 1.11.45
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/CHANGELOG.md +6 -0
- package/components/ClientMain.vue +719 -299
- package/components/DashboardMain.vue +2041 -117
- package/components/DocumentManagement.vue +351 -53
- package/components/InvitationClientForm.vue +23 -10
- package/components/TableV3.vue +179 -0
- package/composables/useOrg.ts +9 -1
- package/composables/useOrganizationSwitcher.ts +11 -0
- package/composables/useUser.ts +36 -30
- package/package.json +1 -1
|
@@ -1,58 +1,125 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-row no-gutters>
|
|
2
|
+
<v-row no-gutters class="pa-0">
|
|
3
3
|
<v-col cols="12" class="mb-2">
|
|
4
|
-
<v-row no-gutters>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
<v-row no-gutters align="center">
|
|
5
|
+
<!-- Left Buttons -->
|
|
6
|
+
<v-col cols="12" md="6" class="d-flex align-center">
|
|
7
|
+
<v-btn
|
|
8
|
+
class="text-none me-2"
|
|
9
|
+
rounded="pill"
|
|
10
|
+
variant="tonal"
|
|
11
|
+
size="large"
|
|
12
|
+
@click="setDocument()"
|
|
13
|
+
v-if="canCreate && canCreateDocument"
|
|
14
|
+
>
|
|
15
|
+
Add Document
|
|
16
|
+
</v-btn>
|
|
17
|
+
|
|
18
|
+
<v-btn
|
|
19
|
+
class="text-none"
|
|
20
|
+
rounded="pill"
|
|
21
|
+
variant="tonal"
|
|
22
|
+
size="large"
|
|
23
|
+
@click="openCreateFolderDialog()"
|
|
24
|
+
v-if="canCreate && canCreateDocument"
|
|
25
|
+
>
|
|
26
|
+
Create Folder
|
|
27
|
+
</v-btn>
|
|
28
|
+
</v-col>
|
|
29
|
+
|
|
30
|
+
<!-- Right Search + Filter -->
|
|
31
|
+
<v-col
|
|
32
|
+
cols="12"
|
|
33
|
+
md="6"
|
|
34
|
+
class="d-flex justify-end align-center mt-4 mt-md-0"
|
|
12
35
|
>
|
|
13
|
-
|
|
14
|
-
|
|
36
|
+
<!-- Search -->
|
|
37
|
+
<v-text-field
|
|
38
|
+
v-model="search"
|
|
39
|
+
placeholder="Search"
|
|
40
|
+
density="comfortable"
|
|
41
|
+
variant="outlined"
|
|
42
|
+
hide-details
|
|
43
|
+
rounded="lg"
|
|
44
|
+
class="me-3"
|
|
45
|
+
style="max-width: 220px"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<v-menu v-model="filterMenu" offset-y>
|
|
49
|
+
<template #activator="{ props }">
|
|
50
|
+
<v-btn
|
|
51
|
+
v-bind="props"
|
|
52
|
+
variant="outlined"
|
|
53
|
+
rounded="lg"
|
|
54
|
+
class="filtertype-btn text-none"
|
|
55
|
+
style="max-width: 220px"
|
|
56
|
+
>
|
|
57
|
+
{{ selectedFileType || 'Filter by File Type' }}
|
|
58
|
+
</v-btn>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<v-card class="filtertype-card">
|
|
62
|
+
<v-list class="pa-0">
|
|
63
|
+
<v-list-item
|
|
64
|
+
v-for="(type, idx) in fileTypes"
|
|
65
|
+
:key="idx"
|
|
66
|
+
@click="selectFileType(type)"
|
|
67
|
+
class="filtertype-item"
|
|
68
|
+
>
|
|
69
|
+
<v-list-item-title class="text-subtitle-2">
|
|
70
|
+
{{ type }}
|
|
71
|
+
</v-list-item-title>
|
|
72
|
+
</v-list-item>
|
|
73
|
+
</v-list>
|
|
74
|
+
</v-card>
|
|
75
|
+
</v-menu>
|
|
76
|
+
</v-col>
|
|
15
77
|
</v-row>
|
|
16
78
|
</v-col>
|
|
17
79
|
<v-col cols="12">
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
border="thin"
|
|
22
|
-
rounded="lg"
|
|
80
|
+
<TableV3
|
|
81
|
+
:headers="headers"
|
|
82
|
+
:items="tableItems"
|
|
23
83
|
:loading="loading"
|
|
84
|
+
:page="page"
|
|
85
|
+
:pages="pages"
|
|
86
|
+
:page-range="pageRange"
|
|
87
|
+
:items-per-page="10"
|
|
88
|
+
@refresh="getDocuments"
|
|
89
|
+
@update:page="page = $event"
|
|
90
|
+
@row-click="tableRowClickHandler"
|
|
24
91
|
>
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
<v-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</
|
|
44
|
-
</
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</
|
|
92
|
+
<template #item.name="{ item }">
|
|
93
|
+
<div class="d-flex align-center ga-2">
|
|
94
|
+
<v-icon size="18" color="grey-darken-2">
|
|
95
|
+
{{
|
|
96
|
+
item.file_type?.toLowerCase() === "folder"
|
|
97
|
+
? "mdi-folder"
|
|
98
|
+
: "mdi-file-document-outline"
|
|
99
|
+
}}
|
|
100
|
+
</v-icon>
|
|
101
|
+
<span>{{ item.name }}</span>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<template #item.date_modified="{ item }">
|
|
106
|
+
<span>{{ item.date_modified || "N/A" }}</span>
|
|
107
|
+
</template>
|
|
108
|
+
|
|
109
|
+
<template #item.remarks="{ item }">
|
|
110
|
+
<span>{{ item.remarks || "N/A" }}</span>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<template #item.action="{ item }">
|
|
114
|
+
<v-btn
|
|
115
|
+
variant="flat"
|
|
116
|
+
class="table-download-btn text-none"
|
|
117
|
+
@click.stop="downloadDocument(item._raw)"
|
|
118
|
+
>
|
|
119
|
+
Download
|
|
120
|
+
</v-btn>
|
|
121
|
+
</template>
|
|
122
|
+
</TableV3>
|
|
56
123
|
</v-col>
|
|
57
124
|
|
|
58
125
|
<!-- Create Dialog -->
|
|
@@ -60,6 +127,73 @@
|
|
|
60
127
|
<DocumentForm @cancel="createDialog = false" @success="successCreate()" />
|
|
61
128
|
</v-dialog>
|
|
62
129
|
|
|
130
|
+
<!-- Create Folder Dialog -->
|
|
131
|
+
<v-dialog v-model="createFolderDialog" width="450" persistent>
|
|
132
|
+
<v-card width="100%">
|
|
133
|
+
<v-toolbar class="pa-0" density="compact">
|
|
134
|
+
<v-row no-gutters class="w-100">
|
|
135
|
+
<v-col cols="12" class="pa-4">
|
|
136
|
+
<span class="font-weight-medium text-h6">Create Folder</span>
|
|
137
|
+
</v-col>
|
|
138
|
+
</v-row>
|
|
139
|
+
</v-toolbar>
|
|
140
|
+
|
|
141
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
|
|
142
|
+
<v-form v-model="validFolderForm">
|
|
143
|
+
<v-col cols="12" class="px-6">
|
|
144
|
+
<InputLabel title="Folder Title" />
|
|
145
|
+
<v-text-field
|
|
146
|
+
v-model.trim="folderTitle"
|
|
147
|
+
density="comfortable"
|
|
148
|
+
:rules="[v => !!v || 'Folder title is required']"
|
|
149
|
+
/>
|
|
150
|
+
</v-col>
|
|
151
|
+
|
|
152
|
+
<v-col cols="12" class="px-6">
|
|
153
|
+
<InputLabel title="Remarks (Optional)" />
|
|
154
|
+
<v-textarea
|
|
155
|
+
v-model.trim="folderRemarks"
|
|
156
|
+
density="comfortable"
|
|
157
|
+
rows="3"
|
|
158
|
+
/>
|
|
159
|
+
</v-col>
|
|
160
|
+
</v-form>
|
|
161
|
+
</v-card-text>
|
|
162
|
+
|
|
163
|
+
<v-toolbar class="pa-0" density="compact">
|
|
164
|
+
<v-row no-gutters class="w-100">
|
|
165
|
+
<v-col cols="6" class="pa-0">
|
|
166
|
+
<v-btn
|
|
167
|
+
block
|
|
168
|
+
variant="text"
|
|
169
|
+
class="text-none"
|
|
170
|
+
size="large"
|
|
171
|
+
@click="createFolderDialog = false"
|
|
172
|
+
:disabled="folderLoading"
|
|
173
|
+
height="48"
|
|
174
|
+
>
|
|
175
|
+
Cancel
|
|
176
|
+
</v-btn>
|
|
177
|
+
</v-col>
|
|
178
|
+
<v-col cols="6" class="pa-0">
|
|
179
|
+
<v-btn
|
|
180
|
+
block
|
|
181
|
+
color="black"
|
|
182
|
+
variant="flat"
|
|
183
|
+
class="text-none"
|
|
184
|
+
size="large"
|
|
185
|
+
@click="handleCreateFolder"
|
|
186
|
+
:loading="folderLoading"
|
|
187
|
+
height="48"
|
|
188
|
+
>
|
|
189
|
+
Submit
|
|
190
|
+
</v-btn>
|
|
191
|
+
</v-col>
|
|
192
|
+
</v-row>
|
|
193
|
+
</v-toolbar>
|
|
194
|
+
</v-card>
|
|
195
|
+
</v-dialog>
|
|
196
|
+
|
|
63
197
|
<!-- Edit Dialog -->
|
|
64
198
|
<v-dialog v-model="editDialog" width="450" persistent>
|
|
65
199
|
<DocumentForm
|
|
@@ -218,10 +352,30 @@ const props = defineProps({
|
|
|
218
352
|
value: "name",
|
|
219
353
|
},
|
|
220
354
|
{
|
|
221
|
-
title: "Type",
|
|
222
|
-
value: "
|
|
355
|
+
title: "File Type",
|
|
356
|
+
value: "file_type",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
title: "File Size",
|
|
360
|
+
value: "file_size",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
title: "Date Created",
|
|
364
|
+
value: "date_created",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
title: "Date Modified",
|
|
368
|
+
value: "date_modified",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
title: "Remarks",
|
|
372
|
+
value: "remarks",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
title: "Action",
|
|
376
|
+
value: "action",
|
|
377
|
+
sortable: false,
|
|
223
378
|
},
|
|
224
|
-
// { title: "Action", value: "action-table" },
|
|
225
379
|
],
|
|
226
380
|
},
|
|
227
381
|
canCreate: {
|
|
@@ -251,7 +405,8 @@ const props = defineProps({
|
|
|
251
405
|
});
|
|
252
406
|
|
|
253
407
|
const { headerSearch } = useLocal();
|
|
254
|
-
const
|
|
408
|
+
const documentService = useDocument();
|
|
409
|
+
const { getAll: _getAllDocuments, deleteById, createFolder, create } = documentService;
|
|
255
410
|
const { getFileUrl } = useFile();
|
|
256
411
|
|
|
257
412
|
const page = ref(1);
|
|
@@ -294,9 +449,52 @@ watchEffect(() => {
|
|
|
294
449
|
}
|
|
295
450
|
});
|
|
296
451
|
|
|
452
|
+
function formatDate(value?: string | null) {
|
|
453
|
+
if (!value) {
|
|
454
|
+
return "N/A";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const dateValue = new Date(value);
|
|
458
|
+
if (Number.isNaN(dateValue.getTime())) {
|
|
459
|
+
return "N/A";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return dateValue.toLocaleString("en-SG", {
|
|
463
|
+
year: "numeric",
|
|
464
|
+
month: "2-digit",
|
|
465
|
+
day: "2-digit",
|
|
466
|
+
hour: "2-digit",
|
|
467
|
+
minute: "2-digit",
|
|
468
|
+
hour12: false,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const tableItems = computed(() => {
|
|
473
|
+
return items.value.map((doc: any) => ({
|
|
474
|
+
...doc,
|
|
475
|
+
_raw: doc,
|
|
476
|
+
file_type: doc.type || "N/A",
|
|
477
|
+
file_size: doc.file_size || doc.size || "0mb",
|
|
478
|
+
date_created: formatDate(doc.created_at || doc.createdAt),
|
|
479
|
+
date_modified: formatDate(doc.updated_at || doc.updatedAt),
|
|
480
|
+
remarks: doc.remarks || "N/A",
|
|
481
|
+
}));
|
|
482
|
+
});
|
|
483
|
+
|
|
297
484
|
const createDialog = ref(false);
|
|
298
485
|
const editDialog = ref(false);
|
|
299
486
|
const previewDialog = ref(false);
|
|
487
|
+
|
|
488
|
+
// Create folder dialog state
|
|
489
|
+
const createFolderDialog = ref(false);
|
|
490
|
+
const folderTitle = ref("");
|
|
491
|
+
const folderRemarks = ref("");
|
|
492
|
+
const folderLoading = ref(false);
|
|
493
|
+
const validFolderForm = ref(false);
|
|
494
|
+
|
|
495
|
+
const fileTypes = ref(["All", "pdf", "csv", "doc"]);
|
|
496
|
+
const selectedFileType = ref("All");
|
|
497
|
+
const filterMenu = ref(false);
|
|
300
498
|
const selectedDocument = ref<TDocument>({
|
|
301
499
|
_id: "",
|
|
302
500
|
name: "",
|
|
@@ -307,11 +505,22 @@ const deleteLoading = ref(false);
|
|
|
307
505
|
const confirmDialog = ref(false);
|
|
308
506
|
const selectedDocumentId = ref<string | null>(null);
|
|
309
507
|
|
|
310
|
-
function tableRowClickHandler(
|
|
311
|
-
selectedDocument.value = data
|
|
508
|
+
function tableRowClickHandler(data: any) {
|
|
509
|
+
selectedDocument.value = (data?._raw || data) as TDocument;
|
|
312
510
|
previewDialog.value = true;
|
|
313
511
|
}
|
|
314
512
|
|
|
513
|
+
function downloadDocument(document?: TDocument) {
|
|
514
|
+
if (!document?.attachment || !process.client) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const fileUrl = getFileUrl(document.attachment);
|
|
519
|
+
if (fileUrl) {
|
|
520
|
+
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
315
524
|
function setDocument({
|
|
316
525
|
mode = "create",
|
|
317
526
|
dialog = true,
|
|
@@ -340,6 +549,51 @@ function successCreate() {
|
|
|
340
549
|
showMessage("Document created successfully!", "success");
|
|
341
550
|
}
|
|
342
551
|
|
|
552
|
+
function openCreateFolderDialog() {
|
|
553
|
+
folderTitle.value = "";
|
|
554
|
+
folderRemarks.value = "";
|
|
555
|
+
createFolderDialog.value = true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function handleCreateFolder() {
|
|
559
|
+
if (!folderTitle.value) {
|
|
560
|
+
showMessage("Folder title is required", "error");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
folderLoading.value = true;
|
|
565
|
+
try {
|
|
566
|
+
if (documentService.createFolder) {
|
|
567
|
+
const resp = await (documentService as any).createFolder({
|
|
568
|
+
name: folderTitle.value,
|
|
569
|
+
remarks: folderRemarks.value,
|
|
570
|
+
site: siteId,
|
|
571
|
+
});
|
|
572
|
+
showMessage(resp?.message || "Folder created successfully!", "success");
|
|
573
|
+
} else if ((documentService as any).create) {
|
|
574
|
+
const resp = await (documentService as any).create({
|
|
575
|
+
name: folderTitle.value,
|
|
576
|
+
remarks: folderRemarks.value,
|
|
577
|
+
type: "folder",
|
|
578
|
+
site: siteId,
|
|
579
|
+
});
|
|
580
|
+
showMessage(resp?.message || "Folder created successfully!", "success");
|
|
581
|
+
} else {
|
|
582
|
+
// fallback: no API available in this service
|
|
583
|
+
showMessage("Folder created (local)", "success");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
createFolderDialog.value = false;
|
|
587
|
+
folderTitle.value = "";
|
|
588
|
+
folderRemarks.value = "";
|
|
589
|
+
await getDocuments();
|
|
590
|
+
} catch (error: any) {
|
|
591
|
+
showMessage(error?.response?._data?.message || "Failed to create folder", "error");
|
|
592
|
+
} finally {
|
|
593
|
+
folderLoading.value = false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
343
597
|
function openDeleteDialog() {
|
|
344
598
|
confirmDialog.value = true;
|
|
345
599
|
message.value = "";
|
|
@@ -372,4 +626,48 @@ function successUpdate() {
|
|
|
372
626
|
getDocuments();
|
|
373
627
|
showMessage("Document updated successfully!", "success");
|
|
374
628
|
}
|
|
629
|
+
|
|
630
|
+
function selectFileType(type: string) {
|
|
631
|
+
selectedFileType.value = type;
|
|
632
|
+
filterMenu.value = false;
|
|
633
|
+
}
|
|
375
634
|
</script>
|
|
635
|
+
|
|
636
|
+
<style scoped>
|
|
637
|
+
.table-download-btn {
|
|
638
|
+
min-width: 106px;
|
|
639
|
+
height: 30px;
|
|
640
|
+
border-radius: 4px;
|
|
641
|
+
background-color: #e3e3e3;
|
|
642
|
+
color: #38475a;
|
|
643
|
+
font-size: 12px;
|
|
644
|
+
font-weight: 500;
|
|
645
|
+
box-shadow: none;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.table-download-btn:hover {
|
|
649
|
+
background-color: #d8d8d8;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.filtertype-btn {
|
|
653
|
+
justify-content: flex-start;
|
|
654
|
+
width: 220px;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.filtertype-card {
|
|
658
|
+
background: #fff;
|
|
659
|
+
border-radius: 8px;
|
|
660
|
+
min-width: 160px;
|
|
661
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
|
|
662
|
+
overflow: hidden;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.filtertype-item {
|
|
666
|
+
border-bottom: 1px solid #f0f0f0;
|
|
667
|
+
padding: 10px 16px;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.filtertype-item:last-child {
|
|
671
|
+
border-bottom: none;
|
|
672
|
+
}
|
|
673
|
+
</style>
|
|
@@ -321,7 +321,7 @@ async function checkEmail(email: string) {
|
|
|
321
321
|
|
|
322
322
|
const memberRes = await getAllByUserId(user._id)
|
|
323
323
|
|
|
324
|
-
userAccess.value = memberRes?.items || []
|
|
324
|
+
userAccess.value = memberRes?.items || []
|
|
325
325
|
} else {
|
|
326
326
|
existingUser.value = null
|
|
327
327
|
userAccess.value = []
|
|
@@ -340,23 +340,36 @@ async function submit() {
|
|
|
340
340
|
submitError.value = ""
|
|
341
341
|
|
|
342
342
|
try {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
343
|
+
|
|
344
|
+
for (const app of invite.value.app) {
|
|
345
|
+
|
|
346
|
+
await inviteUserClient({
|
|
347
|
+
email: invite.value.email,
|
|
348
|
+
|
|
349
|
+
app: [app],
|
|
350
|
+
|
|
351
|
+
role: invite.value.role,
|
|
352
|
+
|
|
353
|
+
org: org.value?._id,
|
|
354
|
+
|
|
355
|
+
siteId: invite.value.site,
|
|
356
|
+
siteName: invite.value.siteName,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
351
359
|
|
|
352
360
|
emit("success", true)
|
|
353
361
|
|
|
354
362
|
} catch (e: any) {
|
|
355
|
-
|
|
363
|
+
|
|
364
|
+
submitError.value =
|
|
365
|
+
e?.response?._data?.message || "Error"
|
|
366
|
+
|
|
356
367
|
} finally {
|
|
368
|
+
|
|
357
369
|
loading.submittingForm = false
|
|
358
370
|
}
|
|
359
371
|
}
|
|
372
|
+
|
|
360
373
|
function cancel() {
|
|
361
374
|
emit("cancel")
|
|
362
375
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card
|
|
3
|
+
width="100%"
|
|
4
|
+
variant="outlined"
|
|
5
|
+
border="thin"
|
|
6
|
+
rounded="lg"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
class="table-v3"
|
|
9
|
+
>
|
|
10
|
+
<v-toolbar density="compact" color="grey-lighten-5">
|
|
11
|
+
<template #prepend>
|
|
12
|
+
<v-btn
|
|
13
|
+
icon
|
|
14
|
+
variant="text"
|
|
15
|
+
density="comfortable"
|
|
16
|
+
class="table-v3__refresh-btn"
|
|
17
|
+
@click="emit('refresh')"
|
|
18
|
+
>
|
|
19
|
+
<v-icon size="28" color="black">mdi-history</v-icon>
|
|
20
|
+
</v-btn>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #append>
|
|
24
|
+
<v-row no-gutters justify="end" align="center">
|
|
25
|
+
<span class="mr-2 text-caption text-fontgray">
|
|
26
|
+
{{ pageRange }}
|
|
27
|
+
</span>
|
|
28
|
+
<v-btn
|
|
29
|
+
icon="mdi-chevron-left"
|
|
30
|
+
variant="text"
|
|
31
|
+
density="comfortable"
|
|
32
|
+
:disabled="!canGoPrev"
|
|
33
|
+
@click="goPrev"
|
|
34
|
+
/>
|
|
35
|
+
<v-btn
|
|
36
|
+
icon="mdi-chevron-right"
|
|
37
|
+
variant="text"
|
|
38
|
+
density="comfortable"
|
|
39
|
+
:disabled="!canGoNext"
|
|
40
|
+
@click="goNext"
|
|
41
|
+
/>
|
|
42
|
+
</v-row>
|
|
43
|
+
</template>
|
|
44
|
+
</v-toolbar>
|
|
45
|
+
|
|
46
|
+
<v-data-table
|
|
47
|
+
:headers="headers"
|
|
48
|
+
:items="items"
|
|
49
|
+
:item-value="itemValue"
|
|
50
|
+
:items-per-page="itemsPerPage"
|
|
51
|
+
fixed-header
|
|
52
|
+
hide-default-footer
|
|
53
|
+
:style="`max-height: calc(100vh - (${offset}px))`"
|
|
54
|
+
@click:row="(_: any, data: any) => emit('row-click', data.item)"
|
|
55
|
+
>
|
|
56
|
+
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
|
57
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
58
|
+
</template>
|
|
59
|
+
</v-data-table>
|
|
60
|
+
|
|
61
|
+
<v-toolbar density="compact" color="grey-lighten-5" class="table-v3__footer">
|
|
62
|
+
<template #append>
|
|
63
|
+
<v-row no-gutters justify="end" align="center">
|
|
64
|
+
<span class="mr-2 text-caption text-fontgray">
|
|
65
|
+
{{ pageRange }}
|
|
66
|
+
</span>
|
|
67
|
+
<v-btn
|
|
68
|
+
icon="mdi-chevron-left"
|
|
69
|
+
variant="text"
|
|
70
|
+
density="comfortable"
|
|
71
|
+
:disabled="!canGoPrev"
|
|
72
|
+
@click="goPrev"
|
|
73
|
+
/>
|
|
74
|
+
<v-btn
|
|
75
|
+
icon="mdi-chevron-right"
|
|
76
|
+
variant="text"
|
|
77
|
+
density="comfortable"
|
|
78
|
+
:disabled="!canGoNext"
|
|
79
|
+
@click="goNext"
|
|
80
|
+
/>
|
|
81
|
+
</v-row>
|
|
82
|
+
</template>
|
|
83
|
+
</v-toolbar>
|
|
84
|
+
</v-card>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script setup lang="ts">
|
|
88
|
+
const props = defineProps({
|
|
89
|
+
headers: {
|
|
90
|
+
type: Array as PropType<Array<Record<string, any>>>,
|
|
91
|
+
required: true,
|
|
92
|
+
},
|
|
93
|
+
items: {
|
|
94
|
+
type: Array as PropType<Array<Record<string, any>>>,
|
|
95
|
+
default: () => [],
|
|
96
|
+
},
|
|
97
|
+
loading: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default: false,
|
|
100
|
+
},
|
|
101
|
+
itemValue: {
|
|
102
|
+
type: String,
|
|
103
|
+
default: "_id",
|
|
104
|
+
},
|
|
105
|
+
itemsPerPage: {
|
|
106
|
+
type: Number,
|
|
107
|
+
default: 20,
|
|
108
|
+
},
|
|
109
|
+
page: {
|
|
110
|
+
type: Number,
|
|
111
|
+
default: 1,
|
|
112
|
+
},
|
|
113
|
+
pages: {
|
|
114
|
+
type: Number,
|
|
115
|
+
default: 0,
|
|
116
|
+
},
|
|
117
|
+
pageRange: {
|
|
118
|
+
type: String,
|
|
119
|
+
default: "-- - -- of --",
|
|
120
|
+
},
|
|
121
|
+
offset: {
|
|
122
|
+
type: Number,
|
|
123
|
+
default: 220,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const emit = defineEmits(["refresh", "update:page", "row-click"]);
|
|
128
|
+
|
|
129
|
+
const internalPage = ref(props.page);
|
|
130
|
+
const canGoPrev = computed(() => internalPage.value > 1);
|
|
131
|
+
const canGoNext = computed(
|
|
132
|
+
() => props.pages > 0 && internalPage.value < props.pages
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
function goPrev() {
|
|
136
|
+
if (!canGoPrev.value) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
internalPage.value -= 1;
|
|
140
|
+
emit("update:page", internalPage.value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function goNext() {
|
|
144
|
+
if (!canGoNext.value) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
internalPage.value += 1;
|
|
148
|
+
emit("update:page", internalPage.value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
watch(
|
|
152
|
+
() => props.page,
|
|
153
|
+
(val) => {
|
|
154
|
+
internalPage.value = val;
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<style scoped>
|
|
160
|
+
.table-v3 :deep(thead th) {
|
|
161
|
+
background-color: #f5f5f5;
|
|
162
|
+
color: #6b7280;
|
|
163
|
+
font-weight: 600 !important;
|
|
164
|
+
font-size: 12px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.table-v3 :deep(tbody td) {
|
|
168
|
+
height: 54px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.table-v3__footer {
|
|
172
|
+
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.table-v3__refresh-btn {
|
|
176
|
+
width: 38px;
|
|
177
|
+
height: 38px;
|
|
178
|
+
}
|
|
179
|
+
</style>
|