wip
Some checks failed
CSGOWTF/csgowtf/pipeline/head There was a failure building this commit

This commit is contained in:
2022-03-26 17:45:14 +01:00
parent 552188c8a9
commit 106ef97ede
7 changed files with 432 additions and 417 deletions

View File

@@ -29,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.1", "@rushstack/eslint-patch": "^1.1.1",
"@types/echarts": "^4.9.13",
"@types/luxon": "^2.3.1", "@types/luxon": "^2.3.1",
"@types/node": "^16.11.26", "@types/node": "^16.11.26",
"@vitejs/plugin-vue": "^2.2.4", "@vitejs/plugin-vue": "^2.2.4",

View File

@@ -6,9 +6,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { GetPlayerValue } from "/src/utils"; import { GetPlayerValue } from "@/utils";
import { useStore } from "vuex";
import { import {
onBeforeMount, onBeforeMount,
onMounted, onMounted,
@@ -29,245 +28,252 @@ import {
import { LineChart } from "echarts/charts"; import { LineChart } from "echarts/charts";
import { UniversalTransition } from "echarts/features"; import { UniversalTransition } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers"; import { CanvasRenderer } from "echarts/renderers";
import type { MatchRounds, MatchStats } from "@/types";
import { useMatchDetailsStore } from "@/stores/matchDetails";
import { useInfoStateStore } from "@/stores/infoState";
export default { const matchDetailsStore = useMatchDetailsStore();
name: "EqValueGraph", const infoStateStore = useInfoStateStore();
setup() {
const store = useStore();
let myChart1, max_rounds; let myChart1: echarts.ECharts, max_rounds: echarts.ECharts;
let valueList = []; let valueList: any[] = [];
let dataList = []; let dataList: any[] = [];
const width = ref( const width = ref(
window.innerWidth >= 800 && window.innerWidth <= 1200 window.innerWidth >= 800 && window.innerWidth <= 1200
? window.innerWidth ? window.innerWidth
: window.innerWidth < 800 : window.innerWidth < 800
? 800 ? 800
: 1200 : 1200
); );
const height = ref((width.value * 1) / 3); const height = ref((width.value * 1) / 3);
const data = reactive({ interface eqTeamPlayer {
rounds: {}, round: string;
team: [], player: string;
eq_team_1: [], eq: number;
eq_team_2: [], }
eq_team_player_1: [],
eq_team_player_2: [],
});
const getTeamPlayer = (stats, team) => { const data = reactive({
let arr = []; rounds: {} as MatchRounds,
for (let i = (team - 1) * 5; i < team * 5; i++) { team: [],
arr.push(stats[i].player.steamid64); eq_team_1: [],
} eq_team_2: [],
eq_team_player_1: [] as eqTeamPlayer[],
eq_team_player_2: [] as eqTeamPlayer[],
});
return arr; const getTeamPlayer = (stats: MatchStats[], team: number) => {
}; let arr = [];
for (let i = (team - 1) * 5; i < team * 5; i++) {
const player = stats[i];
arr.push(player?.player?.steamid64);
}
const parseObject = async () => { return arr;
data.rounds = await GetPlayerValue( };
store,
store.state.matchDetails.match_id
);
if (data.rounds === null) data.rounds = {};
for (const round in data.rounds) { const parseObject = async () => {
for (const player in data.rounds[round]) { const [res, info] = await GetPlayerValue(
for (let p in data.team[0]) { matchDetailsStore.matchDetails.match_id
if (data.team[0][p] === player) { );
data.eq_team_player_1.push({
round: round, if (info.message !== "") infoStateStore.addInfo(info);
player: player, if (res !== null) data.rounds = res;
eq:
data.rounds[round][player][0] + data.rounds[round][player][2], for (const round in data.rounds) {
}); for (const player in data.rounds[round]) {
} for (let p in data.team[0]) {
} if (data.team[0][p] === player) {
for (let p in data.team[1]) { data.eq_team_player_1.push({
if (data.team[1][p] === player) { round: round,
data.eq_team_player_2.push({ player: player,
round: round, eq: data.rounds[round][player][0] + data.rounds[round][player][0],
player: player, });
eq:
data.rounds[round][player][0] + data.rounds[round][player][2],
});
}
}
} }
} }
}; for (let p in data.team[1]) {
if (data.team[1][p] === player) {
const sumArr = (arr) => { data.eq_team_player_2.push({
return arr.reduce( round: round,
(acc, current) => ({ player: player,
...acc, eq: data.rounds[round][player][0] + data.rounds[round][player][2],
[current.round]: (acc[current.round] || 0) + current.eq, });
}), }
{}
);
};
const BuildGraphData = (team_1, team_2, max_rounds) => {
let newArr = [];
const half_point = max_rounds / 2 - 1;
for (let round in team_1) {
if (round <= half_point) {
newArr.push(team_1[round] - team_2[round]);
} else newArr.push(team_2[round] - team_1[round]);
} }
return newArr; }
}; }
};
const optionGen = (dataList, valueList) => { // TODO: REWORK
return {
// Make gradient line here const sumArr = (arr: eqTeamPlayer[]) => {
visualMap: [ return arr.reduce(
{ (acc, current) => ({
show: false, ...acc,
type: "continuous", [current.round]: (acc[current.round] || 0) + current.eq,
seriesIndex: 0, }),
color: ["#3a6e99", "#c3a235"], {}
}, );
], };
tooltip: {
trigger: "axis", const BuildGraphData = (team_1, team_2, max_rounds) => {
formatter: "Round <b>{b0}</b><br />{a0} <b>{c0}</b>", let newArr = [];
const half_point = max_rounds / 2 - 1;
for (let round in team_1) {
if (round <= half_point) {
newArr.push(team_1[round] - team_2[round]);
} else newArr.push(team_2[round] - team_1[round]);
}
return newArr;
};
const optionGen = (dataList, valueList) => {
return {
// Make gradient line here
visualMap: [
{
show: false,
type: "continuous",
seriesIndex: 0,
color: ["#3a6e99", "#c3a235"],
},
],
tooltip: {
trigger: "axis",
formatter: "Round <b>{b0}</b><br />{a0} <b>{c0}</b>",
},
xAxis: [
{
type: "category",
data: dataList,
},
],
yAxis: [{}],
grid: [
{
bottom: "10%",
},
{
top: "0%",
},
{
right: "0%",
},
{
left: "0%",
},
],
series: [
{
name: "Net-Worth",
type: "line",
lineStyle: {
width: 4,
}, },
xAxis: [ showSymbol: false,
{ data: valueList,
type: "category", markArea: {
data: dataList, data: [
}, [
], {
yAxis: [{}], name: "Half-Point",
grid: [ xAxis: max_rounds / 2 - 1,
{ label: {
bottom: "10%", color: "white",
}, },
{
top: "0%",
},
{
right: "0%",
},
{
left: "0%",
},
],
series: [
{
name: "Net-Worth",
type: "line",
lineStyle: {
width: 4,
},
showSymbol: false,
data: valueList,
markArea: {
data: [
[
{
name: "Half-Point",
xAxis: max_rounds / 2 - 1,
label: {
color: "white",
},
},
{
xAxis: max_rounds / 2,
},
],
],
itemStyle: {
color: "rgba(200,200,200, 0.3)",
}, },
}, {
xAxis: max_rounds / 2,
},
],
],
itemStyle: {
color: "rgba(200,200,200, 0.3)",
}, },
], },
}; },
}; ],
};
};
const disposeCharts = () => { const disposeCharts = () => {
if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) { if (myChart1 != null && myChart1 !== "" && myChart1 !== undefined) {
myChart1.dispose(); myChart1.dispose();
} }
}; };
const buildCharts = () => { const buildCharts = () => {
disposeCharts(); disposeCharts();
myChart1 = echarts.init( myChart1 = echarts.init(
document.getElementById("economy-graph"), document.getElementById("economy-graph"),
{}, {},
{ {
width: width.value, width: width.value,
height: height.value, height: height.value,
} }
); );
myChart1.setOption(optionGen(dataList, valueList)); myChart1.setOption(optionGen(dataList, valueList));
}; };
onBeforeMount(() => { onBeforeMount(() => {
max_rounds = store.state.matchDetails.max_rounds max_rounds = store.state.matchDetails.max_rounds
? store.state.matchDetails.max_rounds ? store.state.matchDetails.max_rounds
: 30; : 30;
}); });
onMounted(() => { onMounted(() => {
if (store.state.matchDetails.stats) { if (store.state.matchDetails.stats) {
echarts.use([ echarts.use([
TitleComponent, TitleComponent,
TooltipComponent, TooltipComponent,
GridComponent, GridComponent,
VisualMapComponent, VisualMapComponent,
LineChart, LineChart,
CanvasRenderer, CanvasRenderer,
UniversalTransition, UniversalTransition,
MarkAreaComponent, MarkAreaComponent,
]); ]);
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 1)); data.team.push(getTeamPlayer(store.state.matchDetails.stats, 1));
data.team.push(getTeamPlayer(store.state.matchDetails.stats, 2)); data.team.push(getTeamPlayer(store.state.matchDetails.stats, 2));
parseObject(); parseObject();
} }
}); });
onUnmounted(() => { onUnmounted(() => {
disposeCharts(); disposeCharts();
}); });
watch( watch(
() => data.rounds, () => data.rounds,
() => { () => {
data.eq_team_1 = sumArr(data.eq_team_player_1); data.eq_team_1 = sumArr(data.eq_team_player_1);
data.eq_team_2 = sumArr(data.eq_team_player_2); data.eq_team_2 = sumArr(data.eq_team_player_2);
valueList = BuildGraphData(data.eq_team_1, data.eq_team_2, max_rounds); valueList = BuildGraphData(data.eq_team_1, data.eq_team_2, max_rounds);
dataList = Array.from(Array(valueList.length + 1).keys()); dataList = Array.from(Array(valueList.length + 1).keys());
dataList.shift(); dataList.shift();
buildCharts(); buildCharts();
} }
); );
window.onresize = () => { window.onresize = () => {
if (window.innerWidth > 1200) { if (window.innerWidth > 1200) {
width.value = 1200; width.value = 1200;
} }
if (window.innerWidth <= 1200 && window.innerWidth >= 800) { if (window.innerWidth <= 1200 && window.innerWidth >= 800) {
width.value = window.innerWidth - 20; width.value = window.innerWidth - 20;
} }
if (window.innerWidth < 800) { if (window.innerWidth < 800) {
width.value = 800; width.value = 800;
} }
height.value = (width.value * 1) / 3; height.value = (width.value * 1) / 3;
buildCharts(); buildCharts();
};
},
}; };
</script> </script>

View File

@@ -35,6 +35,7 @@
</template> </template>
<script> <script>
// TODO: REWORK
import * as echarts from "echarts/core"; import * as echarts from "echarts/core";
import { import {
GridComponent, GridComponent,

View File

@@ -41,13 +41,13 @@
(m.vac && (m.vac &&
FormatVacDate( FormatVacDate(
m.vac_date, m.vac_date,
store.state.matchDetails.date matchDetailsStore.matchDetails.date
) !== '') || ) !== '') ||
(!m.vac && (!m.vac &&
m.game_ban && m.game_ban &&
FormatVacDate( FormatVacDate(
m.game_ban_date, m.game_ban_date,
store.state.matchDetails.date matchDetailsStore.matchDetails.date
) !== '') ) !== '')
? 'ban-shadow' ? 'ban-shadow'
: '' : ''
@@ -57,11 +57,11 @@
? 'Game-banned: ' + ? 'Game-banned: ' +
FormatVacDate( FormatVacDate(
m.game_ban_date, m.game_ban_date,
store.state.matchDetails.date matchDetailsStore.matchDetails.date
) )
: m.vac && !m.game_ban : m.vac && !m.game_ban
? 'Vac-banned: ' + ? 'Vac-banned: ' +
FormatVacDate(m.vac_date, store.state.matchDetails.date) FormatVacDate(m.vac_date, matchDetailsStore.matchDetails.date)
: '' : ''
" "
> >
@@ -99,8 +99,7 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { useStore } from "vuex";
import { onMounted, reactive } from "vue"; import { onMounted, reactive } from "vue";
import { import {
constructAvatarUrl, constructAvatarUrl,
@@ -109,131 +108,119 @@ import {
GetChatHistory, GetChatHistory,
GoToPlayer, GoToPlayer,
truncate, truncate,
} from "/src/utils"; } from "@/utils";
import TranslateChatButton from "/src/components/TranslateChatButton"; import TranslateChatButton from "@/components/TranslateChatButton.vue";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import {useMatchDetailsStore} from "@/stores/matchDetails";
import {useInfoStateStore} from "@/stores/infoState";
import type {MatchChat, MatchChatItem, MatchStats} from "@/types";
export default { const matchDetailsStore = useMatchDetailsStore();
name: "MatchChatHistory", const infoStoreState = useInfoStateStore();
components: { TranslateChatButton },
setup() {
const store = useStore();
const data = reactive({ interface ChatStats extends MatchStats, MatchChatItem {}
chat: [],
translatedText: [],
originalChat: [],
clientWidth: 0,
});
const handleTranslatedText = async (e) => { const data = reactive({
const [res, toggle] = await e; chat: [] as ChatStats[],
translatedText: [] as ChatStats[],
originalChat: [],
clientWidth: 0,
});
if (res !== null) { const handleTranslatedText = async (e: PromiseLike<[any, any]> | [any, any]) => {
if (toggle === "translated") { const [res, toggle] = await e;
data.translatedText = await setPlayer(sortChatHistory(res, true));
data.chat = data.translatedText;
} else if (toggle === "original") {
data.chat = data.originalChat;
}
}
};
const getChatHistory = async () => { if (res !== null) {
const resData = await GetChatHistory( if (toggle === "translated") {
store, data.translatedText = await setPlayer(sortChatHistory(res, true));
store.state.matchDetails.match_id data.chat = data.translatedText;
); } else if (toggle === "original") {
if (resData !== null) { data.chat = data.originalChat;
data.chat = await setPlayer(sortChatHistory(resData)); }
data.originalChat = data.chat; }
}
};
const sortChatHistory = (res = {}, translated = false) => {
let arr = [];
if (res !== {}) {
Object.keys(res).forEach((i) => {
res[i].forEach((o) => {
let obj = Object.assign({
player: i,
tick: o.tick,
all_chat: o.all_chat,
message: o.message,
translated_from: translated ? o.translated_from : null,
translated_to: translated ? o.translated_to : null,
});
arr.push(obj);
});
});
}
arr.sort((a, b) => a.tick - b.tick);
return arr;
};
const setPlayer = async (chat) => {
let arr = [];
for (const o of chat) {
for (const p of store.state.matchDetails.stats) {
if (o.player === p.player.steamid64) {
const obj = Object.assign({
player: truncate(p.player.name, 20),
steamid64: p.player.steamid64,
avatar: p.player.avatar,
color: p.color,
startSide: p.team_id,
tracked: p.player.tracked,
vac: p.player.vac,
vac_date: p.player.vac_date,
game_ban: p.player.game_ban,
game_ban_date: p.player.game_ban_date,
tick: o.tick,
tick_rate:
store.state.matchDetails.tick_rate &&
store.state.matchDetails.tick_rate !== -1
? store.state.matchDetails.tick_rate
: 64,
all_chat: o.all_chat,
message: o.message,
translated_from: o.translated_from,
translated_to: o.translated_to,
});
arr.push(obj);
}
}
}
return arr;
};
const sizeTable = () => {
if (document.documentElement.clientWidth <= 768) {
data.clientWidth = document.documentElement.clientWidth - 32;
} else {
data.clientWidth = 700;
}
};
window.onresize = () => {
sizeTable();
};
onMounted(() => {
getChatHistory();
sizeTable();
});
return {
data,
store,
ISO6391,
constructAvatarUrl,
GoToPlayer,
ConvertTickToTime,
FormatVacDate,
handleTranslatedText,
};
},
}; };
const getChatHistory = async () => {
const [resData, info] = await GetChatHistory(matchDetailsStore.matchDetails.match_id);
if (info.message !== "") infoStoreState.addInfo(info);
if (resData !== null) {
data.chat = await setPlayer(sortChatHistory(resData));
data.originalChat = data.chat;
}
};
const sortChatHistory = (res: MatchChat = {}, translated = false): MatchChatItem[] => {
let arr = [] as MatchChatItem[];
if (res !== {}) {
Object.keys(res).forEach((i) => {
res[i].forEach((o) => {
let obj = Object.assign({
player: i,
tick: o.tick,
all_chat: o.all_chat,
message: o.message,
translated_from: translated ? o.translated_from : null,
translated_to: translated ? o.translated_to : null,
});
arr.push(obj);
});
});
}
arr.sort((a, b) => a.tick - b.tick);
return arr;
};
const setPlayer = async (chat: MatchChatItem[]): ChatStats[] => {
let arr: ChatStats[] = [];
for (const o of chat) {
for (const p of matchDetailsStore.matchDetails.stats || []) {
if (o.player === p.player?.steamid64) {
const obj: ChatStats = Object.assign({
player: truncate(p.player?.name || "", 20),
steamid64: p.player?.steamid64,
avatar: p.player?.avatar,
color: p.color,
startSide: p.team_id,
tracked: p.player?.tracked,
vac: p.player?.vac,
vac_date: p.player?.vac_date,
game_ban: p.player?.game_ban,
game_ban_date: p.player?.game_ban_date,
tick: o.tick,
tick_rate:
matchDetailsStore.matchDetails.tick_rate &&
matchDetailsStore.matchDetails.tick_rate !== -1
? matchDetailsStore.matchDetails.tick_rate
: 64,
all_chat: o.all_chat,
message: o.message,
translated_from: o.translated_from,
translated_to: o.translated_to,
});
arr.push(obj);
}
}
}
return arr;
};
const sizeTable = () => {
if (document.documentElement.clientWidth <= 768) {
data.clientWidth = document.documentElement.clientWidth - 32;
} else {
data.clientWidth = 700;
}
};
window.onresize = () => {
sizeTable();
};
onMounted(() => {
getChatHistory();
sizeTable();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -22,86 +22,89 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { onMounted, reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { GetChatHistoryTranslated } from "/src/utils"; import { GetChatHistoryTranslated } from "@/utils";
import { useStore } from "vuex"; import { useMatchDetailsStore } from "@/stores/matchDetails";
import { useInfoStateStore } from "@/stores/infoState";
export default { const matchDetailsStore = useMatchDetailsStore();
name: "TranslateChatButton", const infoStateStore = useInfoStateStore();
props: {
translated: {
type: Boolean,
required: true,
},
},
setup() {
const store = useStore();
const data = reactive({ // TODO: Maybe remove props
browserIsoCode: "", const props = defineProps<{
browserLangCode: "", translated: boolean;
browserLang: "", }>();
});
const toggle = ref("original"); // TODO: needs more work
const emit = defineEmits<{
(e: "translate", ): [MatchChat || null, string]
}>();
const setLanguageVariables = () => { const data = reactive({
const navLangs = navigator.languages; browserIsoCode: "",
browserLangCode: "",
browserLang: "",
});
data.browserIsoCode = navLangs.find((l) => l.length === 5); const toggle = ref("original");
data.browserLangCode = navLangs[0];
if (ISO6391.validate(data.browserLangCode)) { const setLanguageVariables = () => {
data.browserLang = ISO6391.getNativeName(data.browserLangCode); const navLangs = navigator.languages;
} else {
data.browserIsoCode = "en-US";
data.browserLangCode = "en";
data.browserLang = "English";
}
};
const handleBtnClick = async () => { data.browserIsoCode = navLangs.find((l) => l.length === 5) || "";
let response; data.browserLangCode = navLangs[0];
const refreshButton = document.querySelector(".loading-icon .fa-spinner"); if (ISO6391.validate(data.browserLangCode)) {
refreshButton.classList.add("show"); data.browserLang = ISO6391.getNativeName(data.browserLangCode);
} else {
toggleShow(); data.browserIsoCode = "en-US";
data.browserLangCode = "en";
response = await GetChatHistoryTranslated( data.browserLang = "English";
store, }
store.state.matchDetails.match_id
);
if (refreshButton.classList.contains("show"))
refreshButton.classList.remove("show");
return [response, toggle.value];
};
const toggleShow = () => {
const offBtn = document.getElementById("toggle-off");
const onBtn = document.getElementById("toggle-on");
if (offBtn.classList.contains("show")) {
offBtn.classList.remove("show");
onBtn.classList.add("show");
toggle.value = "translated";
} else if (onBtn.classList.contains("show")) {
onBtn.classList.remove("show");
offBtn.classList.add("show");
toggle.value = "original";
}
};
onMounted(() => {
setLanguageVariables();
});
return { data, toggle, handleBtnClick };
},
}; };
const handleBtnClick = async () => {
const refreshButton = document.querySelector(
".loading-icon .fa-spinner"
) as HTMLElement;
refreshButton.classList.add("show");
toggleShow();
// TODO: Needs more work
// TODO: Add langCode
const [response, info] = await GetChatHistoryTranslated(
matchDetailsStore.matchDetails.match_id
);
if (info.message !== "") infoStateStore.addInfo(info);
if (refreshButton.classList.contains("show"))
refreshButton.classList.remove("show");
return [response, toggle.value];
};
const toggleShow = () => {
const offBtn = document.getElementById("toggle-off") as HTMLElement;
const onBtn = document.getElementById("toggle-on") as HTMLElement;
if (offBtn.classList.contains("show")) {
offBtn.classList.remove("show");
onBtn.classList.add("show");
toggle.value = "translated";
} else if (onBtn.classList.contains("show")) {
onBtn.classList.remove("show");
offBtn.classList.add("show");
toggle.value = "original";
}
};
onMounted(() => {
setLanguageVariables();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,14 +1,14 @@
import type { Player } from "@/types/Player"; import type { Player } from "@/types/Player";
export interface MatchChat { export interface MatchChat {
[key: string]: [ [key: string]: MatchChatItem[];
{ }
player?: Player;
message: string; export interface MatchChatItem {
all_chat: boolean; player?: Player;
tick: number; message: string;
translated_from?: string; all_chat: boolean;
translated_to?: string; tick: number;
} translated_from?: string;
]; translated_to?: string;
} }

View File

@@ -141,6 +141,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/echarts@npm:^4.9.13":
version: 4.9.13
resolution: "@types/echarts@npm:4.9.13"
dependencies:
"@types/zrender": "*"
checksum: 19e9d6098cab817a58f949541c5f9642f77dd535ca1413128f33045db631c8ea95fe4fa2c1ff2d3f679a9b4f68d52da70c47ce59338b336dffbe468ac3b79c03
languageName: node
linkType: hard
"@types/json-schema@npm:^7.0.9": "@types/json-schema@npm:^7.0.9":
version: 7.0.10 version: 7.0.10
resolution: "@types/json-schema@npm:7.0.10" resolution: "@types/json-schema@npm:7.0.10"
@@ -162,6 +171,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/zrender@npm:*":
version: 4.0.1
resolution: "@types/zrender@npm:4.0.1"
checksum: 2d18f65241a10232d1600359821ff6ace09afee75e52d6b44f4ddc7244af6915c5b944857cd580955ae213f67f8d07187326a8bc94ef1ebd2807cc23921c548c
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:^5.0.0": "@typescript-eslint/eslint-plugin@npm:^5.0.0":
version: 5.15.0 version: 5.15.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.15.0" resolution: "@typescript-eslint/eslint-plugin@npm:5.15.0"
@@ -897,6 +913,7 @@ __metadata:
dependencies: dependencies:
"@popperjs/core": ^2.11.4 "@popperjs/core": ^2.11.4
"@rushstack/eslint-patch": ^1.1.1 "@rushstack/eslint-patch": ^1.1.1
"@types/echarts": ^4.9.13
"@types/luxon": ^2.3.1 "@types/luxon": ^2.3.1
"@types/node": ^16.11.26 "@types/node": ^16.11.26
"@vitejs/plugin-vue": ^2.2.4 "@vitejs/plugin-vue": ^2.2.4