feat:#3 frontend room occupancy decoder

This commit is contained in:
survellow
2024-05-26 23:28:44 +02:00
parent 5ca1f27edd
commit 7575286cf4
16 changed files with 665 additions and 8 deletions

View File

@ -16,7 +16,9 @@
"@tanstack/vue-query": "^5.28.9",
"@tanstack/vue-query-devtools": "^5.28.10",
"@vueuse/core": "^10.9.0",
"bson": "^5.5.1",
"country-flag-emoji-polyfill": "^0.1.8",
"date-fns": "^3.6.0",
"pinia": "^2.1.7",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",
@ -4220,6 +4222,14 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bson": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
"engines": {
"node": ">=14.20.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -4562,6 +4572,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",

View File

@ -21,7 +21,9 @@
"@tanstack/vue-query": "^5.28.9",
"@tanstack/vue-query-devtools": "^5.28.10",
"@vueuse/core": "^10.9.0",
"bson": "^5.5.1",
"country-flag-emoji-polyfill": "^0.1.8",
"date-fns": "^3.6.0",
"pinia": "^2.1.7",
"primeflex": "^3.3.1",
"primeicons": "^6.0.1",

View File

@ -0,0 +1,44 @@
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
//Copyright (C) 2024 HTWKalender support@htwkalender.de
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU Affero General Public License for more details.
//You should have received a copy of the GNU Affero General Public License
//along with this program. If not, see <https://www.gnu.org/licenses/>.
import { BSON } from "bson";
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
export async function fetchRoomOccupancy(
from_date: string,
to_date: string,
): Promise<RoomOccupancyList> {
var roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
new Date(), 0, 0, []
);
await fetch(
"/api/schedule/rooms?from=" + from_date + "&to=" + to_date,
)
.then((response) => {
return response.arrayBuffer();
})
.then((roomsResponse: ArrayBuffer | null) => {
if (roomsResponse == null) {
return null;
}
const data = new Uint8Array(roomsResponse);
roomOccupancyList = BSON.deserialize(data) as RoomOccupancyList;
return roomOccupancyList;
});
return roomOccupancyList;
}

View File

@ -51,6 +51,11 @@ const items = computed(() => [
icon: "pi pi-fw pi-calendar",
route: "/rooms/free",
},
{
label: t("roomFinderPage.roomSchedule") + " (offline)",
icon: "pi pi-fw pi-ban",
route: "/rooms/occupancy/offline",
}
],
},
{

View File

@ -30,6 +30,7 @@ import router from "@/router";
import { formatYearMonthDay } from "@/helpers/dates";
import { useQuery } from "@tanstack/vue-query";
import { watch } from "vue";
import { isValid } from "date-fns";
const { t } = useI18n({ useScope: "global" });
@ -50,11 +51,13 @@ function setDateFromQuery() {
return;
}
// date is in format like YYYYMMDD
// TODO check if date is valid
const year = queryDate.substring(0, 4);
const month = queryDate.substring(4, 6);
const day = queryDate.substring(6, 8);
date.value = new Date(`${year}-${month}-${day}`);
if (!isValid(date.value)) {
date.value = new Date();
}
}
}

View File

@ -0,0 +1,203 @@
<!--
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
Copyright (C) 2024 HTWKalender support@htwkalender.de
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts" setup>
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import { computed, ComputedRef, inject, ref, Ref } from "vue";
import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core";
import { useI18n } from "vue-i18n";
import allLocales from "@fullcalendar/core/locales-all";
import router from "@/router";
import { formatYearMonthDay } from "@/helpers/dates";
import { useQuery } from "@tanstack/vue-query";
import { watch } from "vue";
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy";
import { isValid } from "date-fns";
const { t } = useI18n({ useScope: "global" });
const props = defineProps({
room: {
type: String,
required: true,
},
});
const date: Ref<Date> = ref(new Date());
// Set the selected date from the URL
function setDateFromQuery() {
const queryDate = router.currentRoute.value.query.date;
if (typeof queryDate === "string") {
if (queryDate === formatYearMonthDay(date.value)) {
return;
}
// date is in format like YYYYMMDD
const year = queryDate.substring(0, 4);
const month = queryDate.substring(4, 6);
const day = queryDate.substring(6, 8);
date.value = new Date(`${year}-${month}-${day}`);
if (!isValid(date.value)) {
date.value = new Date();
}
}
}
setDateFromQuery();
const currentDateFrom: Ref<string> = ref("");
const currentDateTo: Ref<string> = ref("");
const mobilePage = inject("mobilePage") as Ref<boolean>;
const selectedRoom = computed(() => props.room);
const { data: occupations } = useQuery({
queryKey: ["roomOccupation", selectedRoom, currentDateFrom, currentDateTo],
queryFn: () =>
fetchRoomOccupancy(
new Date(currentDateFrom.value).toISOString(),
new Date(currentDateTo.value).toISOString()
),
select: (data) => data
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
.map((event, index) => ({
id: index,
event: event,
})),
enabled: () => selectedRoom.value !== "" && currentDateFrom.value !== "",
staleTime: 5000000, // 500 seconds
});
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
locales: allLocales,
locale: t("languageCode"),
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
// local debugging of mobilePage variable in object creation on ternary expression
initialView: mobilePage.value ? "Day" : "week",
initialDate: date.value,
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
height: "auto",
views: {
week: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
dateAlignment: "week",
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 7 },
firstDay: 1,
allDaySlot: false,
hiddenDays: [0],
},
Day: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 1 },
allDaySlot: false,
hiddenDays: [0],
},
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
datesSet: function (dateInfo: DatesSetArg) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
currentDateFrom.value = startDate.toISOString().split("T")[0];
currentDateTo.value = endDate.toISOString().split("T")[0];
router.replace({
query: {
...router.currentRoute.value.query,
date: formatYearMonthDay(endDate),
},
});
},
events: function (
_info: unknown,
successCallback: (events: EventInput[]) => void,
) {
if (!occupations.value) return;
successCallback(
occupations.value.map((event) => {
return {
id: event.id.toString(),
start: event.event.start,
end: event.event.end,
color: event.event.free
? "var(--htwk-gruen-500)"
: "var(--htwk-grau-60-500)",
textColor: event.event.free
? "var(--green-50)"
: "white",
title: event.event.stub
? t("roomFinderPage.stub")
: event.event.free
? t("roomFinderPage.available")
: t("roomFinderPage.occupied"),
} as EventInput;
}),
);
},
}));
</script>
<template>
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
</template>
<style scoped>
:deep(.fc-toolbar.fc-header-toolbar) {
flex-wrap: wrap;
justify-content: space-between;
gap: 0.5rem;
}
</style>

View File

@ -26,7 +26,8 @@
"dropDownSelect": "Bitte wähle einen Raum aus",
"noRoomsAvailable": "Keine Räume verfügbar",
"available": "verfügbar",
"occupied": "belegt"
"occupied": "belegt",
"stub": "bitte online prüfen"
},
"freeRooms": {
"freeRooms": "Freie Räume",

View File

@ -26,7 +26,8 @@
"dropDownSelect": "please select a room",
"noRoomsAvailable": "no rooms listed",
"available": "available",
"occupied": "occupied"
"occupied": "occupied",
"stub": "please check online"
},
"freeRooms": {
"freeRooms": "free rooms",

View File

@ -26,7 +26,8 @@
"dropDownSelect": "部屋を選択してください",
"noRoomsAvailable": "利用可能な部屋がありません",
"available": "利用可能",
"occupied": "占有中"
"occupied": "占有中",
"stub": "オンラインでご確認ください。"
},
"freeRooms": {
"freeRooms": "空いている部屋",

View File

@ -31,7 +31,17 @@ export class Event {
) {}
}
export class AnonymizedEventDTO {
export class AnonymizedOccupancy {
constructor(
public start: string,
public end: string,
public rooms: string,
public free: boolean,
public stub: boolean = false,
) {}
}
export class AnonymizedEventDTO extends AnonymizedOccupancy {
constructor(
public day: string,
public week: string,
@ -39,5 +49,7 @@ export class AnonymizedEventDTO {
public end: string,
public rooms: string,
public free: boolean,
) {}
) {
super(start, end, rooms, free);
}
}

View File

@ -0,0 +1,240 @@
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
//Copyright (C) 2024 HTWKalender support@htwkalender.de
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU Affero General Public License for more details.
//You should have received a copy of the GNU Affero General Public License
//along with this program. If not, see <https://www.gnu.org/licenses/>.
import { Binary } from "bson";
import { AnonymizedEventDTO, AnonymizedOccupancy } from "./event";
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, startOfTomorrow, subDays } from "date-fns";
/**
* Represents the occupancy of a single room.
* occupancy is a binary string, where each bit represents a block of time.
*/
class RoomOccupancy {
constructor(
public name : string,
public occupancy : Binary,
) {}
}
/// The start time of the day. 08:00
const START_OF_WORKDAY : Duration = {hours: 8};
/// The end time of the day. 20:00
const END_OF_WORKDAY : Duration = {hours: 20};
/**
* Represents the occupancy of multiple rooms.
* start is the start date of the occupancy list.
* granularity is the duration of a single block in minutes.
* blocks is the number of time slices called blocks.
* rooms is a list of RoomOccupancy objects representing the occupancy of each room.
*/
export class RoomOccupancyList {
constructor(
public start : Date,
public granularity : number,
public blocks : number,
public rooms : RoomOccupancy[],
) {}
/**
* Get a list of all rooms encoded in this occupancy list.
* @returns a list of room names.
*/
public getRooms() : string[] {
return this.rooms.map((room) => room.name);
}
/**
* Decode the occupancy of a room for a given time range.
* @param room the room to decode.
* @param from the start of the time range.
* @param to the end of the time range.
* @returns a list of AnonymizedEventDTO objects representing the occupancy of the room.
*/
public decodeOccupancy(room : string, from : Date, to : Date) : AnonymizedOccupancy[] {
console.log("Decoding occupancy for room " + room + " from " + from.toISOString() + " to " + to.toISOString());
const roomOccupancy = this.rooms.find((r) => r.name === room);
if (roomOccupancy === undefined) {
return RoomOccupancyList.generateStubEvents(room, from, to);
}
const occupancyList = [];
// Get start and end of decoded time range (within encoded list and requested range)
let decodeInterval = interval(clamp(from, this.getOccupancyInterval()), clamp(to, this.getOccupancyInterval()));
// Calculate the slice of bytes, that are needed to decode the requested time range
let minutesFromStart = differenceInMinutes(this.start, decodeInterval.start);
let minutesToEnd = differenceInMinutes(this.start, decodeInterval.end);
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
let lastByte = Math.floor(minutesToEnd / this.granularity / 8);
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
let decodeSlice = roomOccupancy.occupancy.buffer.slice(firstByte, lastByte + 1);
// Decode the occupancy data
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
// add stub events for the time before and after the decoded time range
if (!isEqual(from, decodeInterval.start)) {
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, from, decodeInterval.start));
}
if (!isEqual(to, decodeInterval.end)) {
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to));
}
return occupancyList;
}
/**
* Get the decoded time interval within the current occupancy list.
* @returns the interval of the occupancy list.
*/
private getOccupancyInterval() : NormalizedInterval<Date> {
return interval(this.start, addMinutes(this.start, this.granularity * this.blocks));
}
/**
* Decodes the whole array of occupancy data with the first bit representing the given start time.
* @param occupancy the occupancy data to decode.
* @param start the start time of the occupancy data.
* @param granularity the duration of a single block in minutes.
* @param room the room name.
* @returns a list of AnonymizedOccupancy objects representing the occupancy of the room.
*/
public static decodeOccupancyData(occupancy : Uint8Array, start : Date, granularity : number, room : string) : AnonymizedOccupancy[] {
let occupancyList = [];
let firstOccupancyBit : number | null = null;
// Iterate over all bytes that are in the array
for (let byte_i = 0; byte_i < occupancy.length; byte_i++) {
let byte = occupancy[byte_i];
// Iterate over all bits in the current byte
for (let bit_i = 0; bit_i < 8; bit_i++) {
let isOccupied = (byte & (1 << bit_i)) !== 0;
if (firstOccupancyBit === null && isOccupied) {
firstOccupancyBit = byte_i * 8 + bit_i;
} else if (firstOccupancyBit !== null && !isOccupied) {
let startTime = addMinutes(start, firstOccupancyBit * granularity);
let endTime = addMinutes(start, (byte_i * 8 + bit_i) * granularity);
// add event between start and end of a block of boolean true values
occupancyList.push(new AnonymizedOccupancy(
startTime.toISOString(),
endTime.toISOString(),
room,
true
));
firstOccupancyBit = null;
}
}
}
// add last event if it is still ongoing
if (firstOccupancyBit !== null) {
let startTime = addMinutes(start, firstOccupancyBit * granularity);
let endTime = addMinutes(start, occupancy.length * 8 * granularity);
occupancyList.push(new AnonymizedOccupancy(
startTime.toISOString(),
endTime.toISOString(),
room,
true
));
}
return occupancyList;
}
/**
* Generate a list of AnonymizedOccupancy objects for a given time range.
* The generated events are always lying within the time range [START_OF_DAY, END_OF_DAY].
*
* @param from The start time within the specified start day.
* @param to The end time within the specified end day.
* @returns a list of AnonymizedEventDTO objects, from start to end.
*/
public static generateStubEvents(rooms : string, from : Date, to : Date) : AnonymizedOccupancy[] {
from = RoomOccupancyList.shiftTimeForwardInsideWorkday(from);
to = RoomOccupancyList.shiftTimeBackwardInsideWorkday(to);
if (isAfter(from, to)) {
return [];
}
return eachDayOfInterval({start: from, end: to}).map((day) => {
let startTime = max([from, RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY)]);
let endTime = min([to, RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY)]);
return new AnonymizedOccupancy(
startTime.toISOString(),
endTime.toISOString(),
rooms,
true,
true,
);
});
}
/**
* Shift the time forward to the start of the next day if it is after the end of the current day.
*
* @param date the date time to check if in bounds.
* @returns the shifted time.
*/
private static shiftTimeForwardInsideWorkday(date : Date) : Date {
// if the time of date is after the end of the workday
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
// shift the time to the start of the next day
return startOfDay(addDays(date,1));
} else {
return date;
}
}
/**
* Shift the time backward to the end of the previous day if it is before the start of the current day.
*
* @param date the date time to check if in bounds.
* @returns the shifted time.
*/
private static shiftTimeBackwardInsideWorkday(date : Date) : Date {
// if the time of date is before the start of the workday
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
// shift the time to the end of the previous day
return endOfDay(subDays(date,1));
} else {
return date;
}
}
/**
* Sets the date to the specified time after 00:00.
* @param date the date object to extract the day from.
* @param time the time as Duration after 00:00.
* @returns new date with changed time values.
*/
private static setTimeOfDay(date : Date, time : Duration) : Date {
return add(startOfDay(date), time);
}
}

View File

@ -21,6 +21,7 @@ const AdditionalModules = () => import("../view/AdditionalModules.vue");
const CalendarLink = () => import("../components/CalendarLink.vue");
const RenameModules = () => import("../components/RenameModules.vue");
const RoomFinder = () => import("../view/RoomFinder.vue");
const RoomFinderOffline = () => import("../view/RoomFinderOffline.vue");
const EditCalendarView = () => import("../view/EditCalendarView.vue");
const EditAdditionalModules = () =>
import("../view/editCalendar/EditAdditionalModules.vue");
@ -43,6 +44,11 @@ const router = createRouter({
name: "room-schedule",
component: RoomFinder,
},
{
path: "/rooms/occupancy/offline",
name: "room-schedule-offline",
component: RoomFinderOffline,
},
{
path: "/rooms/free",
name: "free-rooms",

View File

@ -0,0 +1,108 @@
<!--
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
Copyright (C) 2024 HTWKalender support@htwkalender.de
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts" setup>
import { Ref, computed, ref, watch } from "vue";
import { fetchRoom } from "../api/fetchRoom.ts";
import DynamicPage from "./DynamicPage.vue";
import RoomOccupationOffline from "../components/RoomOccupationOffline.vue";
import { computedAsync } from "@vueuse/core";
import router from "@/router";
type Room = {
name: string;
};
const selectedRoom: Ref<Room> = ref({ name: "" });
// Watch for changes in URL parameter
router.afterEach(async (to) => {
const room = to.query.room;
if (room && typeof room === "string") {
setRoomFromList(room, rooms.value);
}
});
const rooms = computedAsync<Set<string>>(async () => {
let rooms: Set<string> = new Set();
return await fetchRoom()
.then((data) => {
rooms = new Set(data);
return rooms;
})
.finally(() => {
const room = router.currentRoute.value.query.room;
if (room && typeof room === "string") {
// check if room is available in roomsList
setRoomFromList(room, rooms);
}
});
}, new Set());
const roomsList = computed(() => {
return Array.from(rooms.value).map((room) => {
return { name: room } as Room;
});
});
/**
* Set the room from the list of rooms
* @param room Name of the room
* @param rooms List of available rooms
*/
function setRoomFromList(room: string, rooms: Set<string>) {
// wait for the roomsList to be available
const roomInList: boolean = rooms.has(room);
if (roomInList) {
selectedRoom.value.name = room;
}
}
watch(selectedRoom, (newRoom: Room) => {
if (newRoom.name !== "") {
router.push({
query: { ...router.currentRoute.value.query, room: newRoom.name },
});
}
});
</script>
<template>
<DynamicPage
:hide-content="selectedRoom.name === ''"
:headline="$t('roomFinderPage.headline') + ' &ndash; Offline'"
:sub-title="$t('roomFinderPage.detail')"
icon="pi pi-search"
>
<template #selection>
<Dropdown
v-model="selectedRoom"
:options="roomsList"
class="flex-1 m-0"
filter
option-label="name"
:placeholder="$t('roomFinderPage.dropDownSelect')"
:empty-message="$t('roomFinderPage.noRoomsAvailable')"
:auto-filter-focus="true"
/>
</template>
<template #content>
<RoomOccupationOffline :room="selectedRoom.name" />
</template>
</DynamicPage>
</template>

View File

@ -21,7 +21,7 @@
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [

View File

@ -96,4 +96,16 @@ export default defineConfig({
},
},
},
esbuild: {
supported: {
'top-level-await': true
},
},
optimizeDeps: {
esbuildOptions: {
supported: {
'top-level-await': true
}
},
},
});