@7365admin1/layer-common 1.11.22 → 1.11.24
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 +12 -0
- package/components/AreaChecklistHistoryMain.vue +1 -1
- package/components/AttendanceMain.vue +6 -6
- package/components/CleaningScheduleMain.vue +18 -8
- package/components/EquipmentItemMain.vue +4 -2
- package/components/EquipmentManagementMain.vue +14 -10
- package/components/ManageChecklistMain.vue +17 -4
- package/components/MemberInformation.vue +49 -13
- package/components/MyAttendanceMain.vue +6 -3
- package/components/PassInformation.vue +197 -144
- package/components/ScanVisitorQRCode.vue +54 -12
- package/components/ScheduleAreaMain.vue +6 -4
- package/components/ScheduleTaskMain.vue +11 -8
- package/components/VisitorDataFromScannedQRCodeForm.vue +292 -0
- package/components/VisitorForm.vue +6 -8
- package/components/VisitorManagement.vue +229 -20
- package/components/VisitorPassKeyQRScanner.vue +138 -0
- package/composables/useVisitor.ts +89 -28
- package/package.json +2 -1
- package/types/visitor.d.ts +45 -15
|
@@ -1,171 +1,224 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</v-
|
|
40
|
-
|
|
2
|
+
<v-row no-gutters class="w-100">
|
|
3
|
+
<v-card class="w-100">
|
|
4
|
+
<v-card-text>
|
|
5
|
+
<v-btn
|
|
6
|
+
block
|
|
7
|
+
color="primary-button"
|
|
8
|
+
:height="40"
|
|
9
|
+
text="Scan QR Code"
|
|
10
|
+
class="text-capitalize"
|
|
11
|
+
disabled
|
|
12
|
+
prepend-icon="mdi-qrcode"
|
|
13
|
+
/>
|
|
14
|
+
<v-autocomplete
|
|
15
|
+
v-model="selectedPass"
|
|
16
|
+
v-model:search="passInput"
|
|
17
|
+
:hide-no-data="false"
|
|
18
|
+
class="mt-3"
|
|
19
|
+
:items="passItems"
|
|
20
|
+
:rules="props.passRules"
|
|
21
|
+
item-title="prefixAndName"
|
|
22
|
+
item-value="_id"
|
|
23
|
+
label="Pass"
|
|
24
|
+
variant="outlined"
|
|
25
|
+
hide-details
|
|
26
|
+
density="compact"
|
|
27
|
+
persistent-hint
|
|
28
|
+
small-chips
|
|
29
|
+
:clearable="props.clearable"
|
|
30
|
+
>
|
|
31
|
+
<template v-slot:chip="{ props, item }">
|
|
32
|
+
<v-chip
|
|
33
|
+
v-if="selectedPass"
|
|
34
|
+
v-bind="props"
|
|
35
|
+
prepend-icon="mdi-card-bulleted-outline"
|
|
36
|
+
:text="item.raw?.prefixAndName"
|
|
37
|
+
></v-chip>
|
|
38
|
+
</template>
|
|
39
|
+
</v-autocomplete>
|
|
40
|
+
|
|
41
|
+
<template v-if="!props.hideKeys">
|
|
42
|
+
<v-autocomplete
|
|
43
|
+
v-model="selectedKeys"
|
|
44
|
+
v-model:search="keyInput"
|
|
45
|
+
:hide-no-data="false"
|
|
46
|
+
class="mt-3"
|
|
47
|
+
:items="keyItems"
|
|
48
|
+
:rules="props.passRules"
|
|
49
|
+
item-title="prefixAndName"
|
|
50
|
+
item-value="_id"
|
|
51
|
+
label="Keys"
|
|
52
|
+
multiple
|
|
53
|
+
variant="outlined"
|
|
54
|
+
hide-details
|
|
55
|
+
density="compact"
|
|
56
|
+
persistent-hint
|
|
57
|
+
small-chips
|
|
58
|
+
:clearable="props.clearable"
|
|
59
|
+
>
|
|
60
|
+
<template v-slot:chip="{ props, item }">
|
|
61
|
+
<v-chip
|
|
62
|
+
v-if="selectedKeys.length > 0"
|
|
63
|
+
v-bind="props"
|
|
64
|
+
prepend-icon="mdi-key"
|
|
65
|
+
:text="item.raw?.prefixAndName"
|
|
66
|
+
></v-chip>
|
|
67
|
+
</template>
|
|
68
|
+
</v-autocomplete>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<v-divider class="my-4 w-100" />
|
|
72
|
+
|
|
73
|
+
<template v-if="selectedType">
|
|
74
|
+
<v-number-input
|
|
75
|
+
v-model="count"
|
|
76
|
+
variant="outlined"
|
|
77
|
+
:min="1"
|
|
78
|
+
:precision="0"
|
|
79
|
+
density="compact"
|
|
80
|
+
:rules="countRules"
|
|
81
|
+
/>
|
|
82
|
+
</template>
|
|
83
|
+
</v-card-text>
|
|
84
|
+
</v-card>
|
|
85
|
+
</v-row>
|
|
41
86
|
</template>
|
|
42
87
|
|
|
43
88
|
<script setup lang="ts">
|
|
44
|
-
import type { PropType } from
|
|
45
|
-
import type { ValidationRule } from
|
|
46
|
-
import usePassKey from
|
|
47
|
-
import useKey from
|
|
89
|
+
import type { PropType } from "vue";
|
|
90
|
+
import type { ValidationRule } from "vuetify/lib/types.mjs";
|
|
91
|
+
import usePassKey from "../composables/usePassKey";
|
|
92
|
+
import useKey from "../composables/useKey";
|
|
48
93
|
|
|
49
94
|
const props = defineProps({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
95
|
+
passRules: {
|
|
96
|
+
type: Array as PropType<ValidationRule[]>,
|
|
97
|
+
default: [],
|
|
98
|
+
},
|
|
99
|
+
countRules: {
|
|
100
|
+
type: Array as PropType<ValidationRule[]>,
|
|
101
|
+
default: [],
|
|
102
|
+
},
|
|
103
|
+
site: {
|
|
104
|
+
type: String,
|
|
105
|
+
required: true,
|
|
106
|
+
},
|
|
107
|
+
type: {
|
|
108
|
+
type: String as PropType<TVisitorType>,
|
|
109
|
+
required: true,
|
|
110
|
+
},
|
|
111
|
+
contractorType: {
|
|
112
|
+
type: String,
|
|
113
|
+
default: "",
|
|
114
|
+
},
|
|
115
|
+
hideKeys: {
|
|
116
|
+
type: Boolean,
|
|
117
|
+
default: false,
|
|
118
|
+
},
|
|
119
|
+
clearable: {
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: false,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const pass = defineModel<TPassKeyPayload[]>("pass", { default: [] });
|
|
126
|
+
const keys = defineModel<TPassKeyPayload[]>("keys", { default: [] });
|
|
127
|
+
const selectedPass = ref<string>("");
|
|
128
|
+
const selectedKeys = ref<string[]>([]);
|
|
129
|
+
const passInput = ref("");
|
|
130
|
+
const keyInput = ref("");
|
|
131
|
+
const passItems = ref<TPassKey[]>([]);
|
|
132
|
+
const keyItems = ref<any[]>([]);
|
|
133
|
+
const selectedType = ref<"qr-pass" | "nfc-card">();
|
|
134
|
+
const count = ref(1);
|
|
135
|
+
|
|
136
|
+
const { getPassKeysByPageSearch } = usePassKey();
|
|
88
137
|
|
|
89
138
|
const typeItems = [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
]
|
|
139
|
+
{
|
|
140
|
+
label: "QR Pass",
|
|
141
|
+
value: "qr-pass",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: "NFC Card",
|
|
145
|
+
value: "nfc-card",
|
|
146
|
+
},
|
|
147
|
+
];
|
|
99
148
|
|
|
100
149
|
const passTypesComputed = computed(() => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} else {
|
|
105
|
-
return ["contractor-pass"]
|
|
106
|
-
}
|
|
150
|
+
if (props.type === "contractor") {
|
|
151
|
+
if (props.contractorType === "property-agent") {
|
|
152
|
+
return ["agent-pass"];
|
|
107
153
|
} else {
|
|
108
|
-
|
|
154
|
+
return ["contractor-pass"];
|
|
109
155
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
} else {
|
|
157
|
+
return ["visitor-pass"];
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const {
|
|
162
|
+
data: passesData,
|
|
163
|
+
refresh: refreshPassesData,
|
|
164
|
+
pending: fetchPassesPending,
|
|
165
|
+
} = await useLazyAsyncData("get-pass-keys", () => {
|
|
166
|
+
return getPassKeysByPageSearch({
|
|
167
|
+
search: passInput.value,
|
|
168
|
+
page: 1,
|
|
169
|
+
limit: 500,
|
|
170
|
+
passTypes: passTypesComputed.value,
|
|
171
|
+
sites: [props.site],
|
|
172
|
+
statuses: ["Available"],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
122
175
|
|
|
123
176
|
watch(passesData, (data: any) => {
|
|
124
|
-
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
passItems.value = data?.items || [];
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const {
|
|
181
|
+
data: keysData,
|
|
182
|
+
refresh: refreshKeysData,
|
|
183
|
+
pending: fetchKeysPending,
|
|
184
|
+
} = await useLazyAsyncData("get-keys", () => {
|
|
185
|
+
return getPassKeysByPageSearch({
|
|
186
|
+
search: keyInput.value,
|
|
187
|
+
statuses: ["Available"],
|
|
188
|
+
passTypes: ["pass-key"],
|
|
189
|
+
page: 1,
|
|
190
|
+
limit: 500,
|
|
191
|
+
sites: [props.site],
|
|
192
|
+
});
|
|
193
|
+
});
|
|
137
194
|
|
|
138
195
|
watch(keysData, (data: any) => {
|
|
139
|
-
|
|
140
|
-
})
|
|
196
|
+
keyItems.value = data?.items || [];
|
|
197
|
+
});
|
|
141
198
|
|
|
142
199
|
watch(selectedPass, (newVal) => {
|
|
143
|
-
|
|
144
|
-
})
|
|
200
|
+
pass.value = [{ keyId: newVal }];
|
|
201
|
+
});
|
|
145
202
|
|
|
146
203
|
watch(selectedKeys, (newVal) => {
|
|
147
|
-
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
keys.value = newVal.map((key) => ({ keyId: key }));
|
|
205
|
+
});
|
|
152
206
|
|
|
153
207
|
//prevent negative value;
|
|
154
208
|
watch(count, (newCount) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
})
|
|
209
|
+
if (newCount < 1) {
|
|
210
|
+
count.value = 1;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
159
213
|
|
|
160
214
|
onMounted(() => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
|
|
215
|
+
if (pass.value.length > 0) {
|
|
216
|
+
selectedPass.value = pass.value[0].keyId;
|
|
217
|
+
}
|
|
218
|
+
if (keys.value.length > 0) {
|
|
219
|
+
selectedKeys.value = keys.value.map((k) => k.keyId);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
169
222
|
</script>
|
|
170
223
|
|
|
171
|
-
<style scoped></style>
|
|
224
|
+
<style scoped></style>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<!-- This is where the QR code scanner will be displayed -->
|
|
33
33
|
</div>
|
|
34
34
|
<!-- Show Error Messages -->
|
|
35
|
-
<div v-if="showNotFoundMessage" class="text-error mt-
|
|
35
|
+
<div v-if="showNotFoundMessage" class="text-error mt-4 text-h5">
|
|
36
36
|
<p>{{ errorMessage }}</p>
|
|
37
37
|
</div>
|
|
38
38
|
<!-- Switch camera button inside the reader box -->
|
|
@@ -52,9 +52,20 @@ const props = defineProps({
|
|
|
52
52
|
type: Boolean,
|
|
53
53
|
default: false,
|
|
54
54
|
},
|
|
55
|
+
site: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: null,
|
|
58
|
+
},
|
|
55
59
|
});
|
|
56
60
|
|
|
57
|
-
const
|
|
61
|
+
const { standardFormatDate } = useUtils();
|
|
62
|
+
|
|
63
|
+
const { validateVisitorQrCode } = useVisitor();
|
|
64
|
+
|
|
65
|
+
const emits = defineEmits([
|
|
66
|
+
"closeDialog",
|
|
67
|
+
"openVisitorDataFromScannedQrCodeDialog",
|
|
68
|
+
]);
|
|
58
69
|
|
|
59
70
|
const message = ref("");
|
|
60
71
|
const messageColor = ref("");
|
|
@@ -86,31 +97,62 @@ const handleScanVisitorQRCode = async () => {
|
|
|
86
97
|
showNotFoundMessage.value = false;
|
|
87
98
|
// Automatically navigate to the scanned URL if it's a valid link
|
|
88
99
|
if (isValidUrl(qrCodeMessage)) {
|
|
89
|
-
console.log(
|
|
90
|
-
|
|
100
|
+
console.log("QR Code value", qrCodeMessage);
|
|
101
|
+
console.log("Last _id", qrCodeMessage.split("/").pop());
|
|
102
|
+
// get the last _id in the url
|
|
103
|
+
const id = qrCodeMessage.split("/").pop();
|
|
104
|
+
// check if it's a valid mongodb _id
|
|
105
|
+
if (/^[0-9a-fA-F]{24}$/.test(id as string)) {
|
|
106
|
+
// validate visitor's QR code
|
|
107
|
+
const response = await validateVisitorQrCode(
|
|
108
|
+
props.site as string,
|
|
109
|
+
id as string
|
|
110
|
+
);
|
|
111
|
+
console.log("validateVisitorQrCode", response);
|
|
112
|
+
if (response.errorMessage) {
|
|
113
|
+
showNotFoundMessage.value = true;
|
|
114
|
+
errorMessage.value =
|
|
115
|
+
response.errorMessage +
|
|
116
|
+
(response.scheduledDate
|
|
117
|
+
? standardFormatDate(response.scheduledDate)
|
|
118
|
+
: "");
|
|
119
|
+
showMessage(errorMessage.value, "error");
|
|
120
|
+
} else {
|
|
121
|
+
closeDialog();
|
|
122
|
+
emits("openVisitorDataFromScannedQrCodeDialog", response);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
showNotFoundMessage.value = true;
|
|
126
|
+
errorMessage.value = "Invalid mongodb _id.";
|
|
127
|
+
showMessage(errorMessage.value, "error");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// window.location.href = qrCodeMessage;
|
|
91
131
|
} else {
|
|
92
132
|
showNotFoundMessage.value = true;
|
|
93
|
-
errorMessage.value = "Invalid url";
|
|
94
|
-
showMessage(
|
|
133
|
+
errorMessage.value = "Invalid url.";
|
|
134
|
+
showMessage(errorMessage.value, "error");
|
|
95
135
|
}
|
|
96
136
|
},
|
|
97
137
|
(msg: string) => {
|
|
98
|
-
if (msg === "QR Code no longer in front") {
|
|
138
|
+
if (msg === "QR Code no longer in front.") {
|
|
99
139
|
showNotFoundMessage.value = true;
|
|
100
140
|
errorMessage.value = msg;
|
|
101
|
-
showMessage(
|
|
141
|
+
showMessage(errorMessage.value, "error");
|
|
102
142
|
}
|
|
103
143
|
}
|
|
104
144
|
)
|
|
105
145
|
.catch((err: any) => {
|
|
146
|
+
closeDialog();
|
|
106
147
|
showMessage(`Unable to start scanning, error: ${err}`, "error");
|
|
107
148
|
});
|
|
108
149
|
};
|
|
109
150
|
|
|
110
|
-
const isValidUrl = (
|
|
151
|
+
const isValidUrl = (url: string) => {
|
|
111
152
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
153
|
+
if (typeof url == "string") {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
114
156
|
} catch (_) {
|
|
115
157
|
return false;
|
|
116
158
|
}
|
|
@@ -130,7 +172,7 @@ const switchCamera = async () => {
|
|
|
130
172
|
};
|
|
131
173
|
|
|
132
174
|
const closeDialog = () => {
|
|
133
|
-
|
|
175
|
+
emits("closeDialog");
|
|
134
176
|
stopScanner();
|
|
135
177
|
};
|
|
136
178
|
|
|
@@ -163,7 +163,6 @@
|
|
|
163
163
|
</template>
|
|
164
164
|
|
|
165
165
|
<script setup lang="ts">
|
|
166
|
-
import { useCleaningSchedulePermission } from "../composables/useCleaningSchedulePermission";
|
|
167
166
|
import useCleaningSchedules from "../composables/useCleaningSchedules";
|
|
168
167
|
import useUtils from "../composables/useUtils";
|
|
169
168
|
|
|
@@ -174,6 +173,9 @@ const props = defineProps({
|
|
|
174
173
|
type: { type: String, required: true, default: "cleaner" },
|
|
175
174
|
serviceType: { type: String, default: "" },
|
|
176
175
|
scheduleRoute: { type: String, default: "cleaning-schedule" },
|
|
176
|
+
canGenerateChecklist: { type: Boolean, default: false },
|
|
177
|
+
canViewHistory: { type: Boolean, default: true },
|
|
178
|
+
canManageScheduleTasks: { type: Boolean, default: false },
|
|
177
179
|
});
|
|
178
180
|
|
|
179
181
|
const submitting = ref(false);
|
|
@@ -190,13 +192,13 @@ const headers = [
|
|
|
190
192
|
{ title: "Status", value: "status" },
|
|
191
193
|
{ title: "Type", value: "type" },
|
|
192
194
|
{ title: "No. of Visits", value: "set" },
|
|
193
|
-
{ title: "Created By", value: "createdByName" },
|
|
194
195
|
];
|
|
195
196
|
|
|
196
197
|
const { formatDate, back } = useUtils();
|
|
197
198
|
|
|
198
|
-
const
|
|
199
|
-
|
|
199
|
+
const canGenerateChecklist = computed(() => props.canGenerateChecklist);
|
|
200
|
+
const canViewHistory = computed(() => props.canViewHistory);
|
|
201
|
+
const canManageScheduleTasks = computed(() => props.canManageScheduleTasks);
|
|
200
202
|
|
|
201
203
|
const selectedScheduleStatus = useState<string>(
|
|
202
204
|
"selectedScheduleStatus",
|
|
@@ -257,24 +257,27 @@
|
|
|
257
257
|
|
|
258
258
|
<script lang="ts" setup>
|
|
259
259
|
import useScheduleTask from "../composables/useScheduleTask";
|
|
260
|
-
import useScheduleTaskPermission from "../composables/useScheduleTaskPermission";
|
|
261
260
|
|
|
262
261
|
const props = defineProps({
|
|
263
262
|
orgId: { type: String, required: true },
|
|
264
263
|
site: { type: String, required: true },
|
|
265
264
|
type: { type: String, default: "toilet" },
|
|
266
265
|
serviceType: { type: String, default: "" },
|
|
266
|
+
canViewScheduleTasks: { type: Boolean, default: true },
|
|
267
|
+
canCreateScheduleTask: { type: Boolean, default: false },
|
|
268
|
+
canUpdateScheduleTask: { type: Boolean, default: false },
|
|
269
|
+
canDeleteScheduleTask: { type: Boolean, default: false },
|
|
270
|
+
canViewScheduleTaskDetails: { type: Boolean, default: true },
|
|
267
271
|
});
|
|
268
272
|
|
|
269
273
|
const { formatDate, debounce } = useUtils();
|
|
270
274
|
const { getScheduleTasks, getScheduleTaskById, deleteScheduleTask } = useScheduleTask();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
} = useScheduleTaskPermission();
|
|
275
|
+
|
|
276
|
+
const canViewScheduleTasks = computed(() => props.canViewScheduleTasks);
|
|
277
|
+
const canCreateScheduleTask = computed(() => props.canCreateScheduleTask);
|
|
278
|
+
const canUpdateScheduleTask = computed(() => props.canUpdateScheduleTask);
|
|
279
|
+
const canDeleteScheduleTask = computed(() => props.canDeleteScheduleTask);
|
|
280
|
+
const canViewScheduleTaskDetails = computed(() => props.canViewScheduleTaskDetails);
|
|
278
281
|
|
|
279
282
|
const page = ref(1);
|
|
280
283
|
const pages = ref(0);
|