@xcpcio/board-app 0.83.2 → 0.85.3
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/dist/404.html +1 -1
- package/dist/assets/Balloon-B3cPN2yD.js +1 -0
- package/dist/assets/Board-BpIhDrix.css +1 -0
- package/dist/assets/Board-CWwqX2IB.js +190 -0
- package/dist/assets/ContestStateBadge-CQOaPmT8.js +1 -0
- package/dist/assets/ContestStateBadge-DgwznQrX.css +1 -0
- package/dist/assets/Countdown-BvWlPnYq.js +1 -0
- package/dist/assets/DataSourceInput-DCNrUj45.js +1 -0
- package/dist/assets/NavBar-O1kqnfE4.css +1 -0
- package/dist/assets/NavBar-t1eXyPKg.js +1 -0
- package/dist/assets/Resolver-CRBstFE3.css +1 -0
- package/dist/assets/Resolver-D_GRj6HP.js +1 -0
- package/dist/assets/RightArrowIcon-F6HBDgDW.js +1 -0
- package/dist/assets/TheInput-Btyuor_6.js +1 -0
- package/dist/assets/Tooltip-C9jfn33Z.css +1 -0
- package/dist/assets/Tooltip-DPDTUI-s.js +27 -0
- package/dist/assets/_..-D9qurBBH.css +1 -0
- package/dist/assets/_...all_-Chjz45WV.js +1 -0
- package/dist/assets/_...all_-TFChZhkr.js +6 -0
- package/dist/assets/_intlify-i18n-0-BmghNkij.js +1 -0
- package/dist/assets/_intlify-i18n-1-DzpDgiG5.js +1 -0
- package/dist/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/assets/balloon-BlTynoB_.js +1 -0
- package/dist/assets/board-A69GsfUB.js +1 -0
- package/dist/assets/board-layout-mk2Y30Yp.js +1 -0
- package/dist/assets/constant-YlOY-yXZ.js +1 -0
- package/dist/assets/countdown-DQRGLfdE.js +1 -0
- package/dist/assets/default-BNqFZJwJ.js +1 -0
- package/dist/assets/headless-BSf0YrvV.js +1 -0
- package/dist/assets/index-C1jdVImx.css +1 -0
- package/dist/assets/index-D57KOcpK.js +12 -0
- package/dist/assets/index-layout-Cg_FpvvD.js +1 -0
- package/dist/assets/pages-DZG1eNR4.css +1 -0
- package/dist/assets/{index-C0JhRtxz.js → pages-kJjvENga.js} +2 -2
- package/dist/assets/pagination-EIqoVe6M.js +21 -0
- package/dist/assets/person-DrN1htE6.js +1 -0
- package/dist/assets/query-CoPl3fiI.js +20 -0
- package/dist/assets/rank-Bjo0QJlM.js +1 -0
- package/dist/assets/rating-B3sXjXNN.js +1 -0
- package/dist/assets/rating-DiCQYzia.css +1 -0
- package/dist/assets/resolver-Db22e0q8.js +1 -0
- package/dist/assets/route-block-ChiJXsfH.js +1 -0
- package/dist/assets/utils-Cfm-h8Z9.js +1 -0
- package/dist/assets/vue.runtime.esm-bundler-DTMOfGWR.js +4 -0
- package/dist/index.html +1 -1
- package/package.json +45 -45
- package/src/auto-imports.d.ts +4 -4
- package/src/components/board/Board.vue +7 -4
- package/src/components/board/Export.vue +5 -1
- package/src/components/board/PendingPage.vue +85 -47
- package/src/components/board/Progress.vue +1 -0
- package/src/components/board/SeatMapView.vue +265 -0
- package/src/components/board/SubmissionsTable.vue +1 -2
- package/src/components/board/SubmitHeatMap.less +11 -3
- package/src/components/board/SubmitHeatMap.vue +56 -86
- package/src/components/board/TeamCard.vue +6 -6
- package/src/components/board/TeamOverview.vue +1 -0
- package/src/components/common/SegmentedControl.vue +87 -0
- package/src/components/flowbite/Tooltip.vue +79 -13
- package/src/components.d.ts +2 -1
- package/src/composables/useQueryBoardData.ts +18 -0
- package/dist/assets/Balloon.vue_vue_type_script_setup_true_lang-CZ_Wx4To.js +0 -1
- package/dist/assets/Board-B0CAQsJI.css +0 -1
- package/dist/assets/Board-BVDDayjC.js +0 -194
- package/dist/assets/ContestStateBadge-D6uf8wH-.css +0 -1
- package/dist/assets/ContestStateBadge-DV_spC5E.js +0 -1
- package/dist/assets/Countdown-BlEqPiY_.js +0 -1
- package/dist/assets/DataSourceInput.vue_vue_type_script_setup_true_lang-CwU_Xgr1.js +0 -1
- package/dist/assets/NavBar-DCPwz9qZ.js +0 -1
- package/dist/assets/NavBar-aT2xEvYz.css +0 -1
- package/dist/assets/Resolver-CY9BFOM6.js +0 -1
- package/dist/assets/Resolver-DV9FleNP.css +0 -1
- package/dist/assets/RightArrowIcon-CtYUhYc6.js +0 -1
- package/dist/assets/TheInput.vue_vue_type_script_setup_true_lang-DPzm-iVE.js +0 -1
- package/dist/assets/Tooltip-DzyPDwbo.css +0 -1
- package/dist/assets/Tooltip.vue_vue_type_script_setup_true_lang-DONjqLXY.js +0 -27
- package/dist/assets/_..-5I0UWqRG.css +0 -1
- package/dist/assets/_...all_-B7l1949F.js +0 -6
- package/dist/assets/_...all_-T0zBVu5S.js +0 -1
- package/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
- package/dist/assets/board-layout-B5DfJEXx.js +0 -1
- package/dist/assets/constant-C2gH6ZII.js +0 -1
- package/dist/assets/dayjs-DjxrjdEm.js +0 -1
- package/dist/assets/default-B200Q7K-.js +0 -1
- package/dist/assets/en-BEousmNN.js +0 -1
- package/dist/assets/headless-OPBem1cy.js +0 -1
- package/dist/assets/index-B0gZ0sxU.js +0 -15
- package/dist/assets/index-BSgd4Lsj.js +0 -1
- package/dist/assets/index-Bk7pJUWz.js +0 -1
- package/dist/assets/index-Bojw84ll.css +0 -1
- package/dist/assets/index-D-lXJTYs.css +0 -1
- package/dist/assets/index-D-msPqye.css +0 -1
- package/dist/assets/index-DqbvaKi_.js +0 -1
- package/dist/assets/index-layout-DBiZ6YWC.js +0 -1
- package/dist/assets/index-uFy-gvEE.js +0 -1
- package/dist/assets/index-uV5YmJst.js +0 -1
- package/dist/assets/pagination-DQg-v0kh.js +0 -3
- package/dist/assets/person-EUz_T6aJ.js +0 -1
- package/dist/assets/rank-BUkksYCC.js +0 -1
- package/dist/assets/route-block-B_A1xBdJ.js +0 -1
- package/dist/assets/use-vmodel-Cl0DXIYt.js +0 -20
- package/dist/assets/zh-CN-DB4l8OJD.js +0 -1
- package/src/components/board/HeatMapTooltip.less +0 -115
- package/src/components/board/HeatMapTooltip.vue +0 -68
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Rank } from "@xcpcio/core";
|
|
2
|
+
import type { Rank, Submission } from "@xcpcio/core";
|
|
3
3
|
import { isAccepted, isRejected } from "@xcpcio/core";
|
|
4
|
+
import "./SubmitHeatMap.less";
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
rank: Rank;
|
|
@@ -9,23 +10,32 @@ const props = defineProps<{
|
|
|
9
10
|
const { t } = useI18n();
|
|
10
11
|
|
|
11
12
|
const rank = computed(() => props.rank);
|
|
13
|
+
const submissions = computed(() => rank.value.getSubmissions());
|
|
12
14
|
|
|
13
15
|
const mapTimeDiffLength = 20;
|
|
14
|
-
const startTime = rank.value.contest.getStartTime();
|
|
15
|
-
const endTime = rank.value.contest.getEndTime();
|
|
16
|
-
const totalDuration = endTime.diff(startTime, "minute");
|
|
17
|
-
const intervalSize = totalDuration / mapTimeDiffLength;
|
|
18
16
|
|
|
19
|
-
const
|
|
17
|
+
const intervalSize = computed(() => {
|
|
18
|
+
const startTime = rank.value.contest.getStartTime();
|
|
19
|
+
const endTime = rank.value.contest.getEndTime();
|
|
20
|
+
const totalDuration = endTime.diff(startTime, "minute");
|
|
21
|
+
|
|
22
|
+
if (!Number.isFinite(totalDuration) || totalDuration <= 0) {
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
const start = index * intervalSize;
|
|
23
|
-
const end = (index + 1) * intervalSize;
|
|
24
|
-
return `Between ${start} and ${end} minutes: `;
|
|
26
|
+
return totalDuration / mapTimeDiffLength;
|
|
25
27
|
});
|
|
26
28
|
|
|
29
|
+
const intervalDescriptions = computed(() =>
|
|
30
|
+
Array.from({ length: mapTimeDiffLength }, (_, index) => {
|
|
31
|
+
const start = index * intervalSize.value;
|
|
32
|
+
const end = (index + 1) * intervalSize.value;
|
|
33
|
+
return `Between ${start} and ${end} minutes: `;
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
27
37
|
const submissionsByProblemId = computed(() => {
|
|
28
|
-
const result = new Map();
|
|
38
|
+
const result = new Map<string, { correct: Submission[]; incorrect: Submission[] }>();
|
|
29
39
|
|
|
30
40
|
rank.value.contest.problems.forEach((p) => {
|
|
31
41
|
result.set(p.id, { correct: [], incorrect: [] });
|
|
@@ -47,64 +57,30 @@ const submissionsByProblemId = computed(() => {
|
|
|
47
57
|
return result;
|
|
48
58
|
});
|
|
49
59
|
|
|
50
|
-
function generateHeatMap(submissions:
|
|
60
|
+
function generateHeatMap(submissions: Submission[], type: "correct" | "incorrect") {
|
|
51
61
|
const counts = Array.from<number>({ length: mapTimeDiffLength }).fill(0);
|
|
62
|
+
const size = intervalSize.value > 0 ? intervalSize.value : 1;
|
|
63
|
+
const descriptions = intervalDescriptions.value;
|
|
52
64
|
|
|
53
65
|
submissions.forEach((s) => {
|
|
54
|
-
const index = Math.min(Math.floor(s.timestampToMinute /
|
|
66
|
+
const index = Math.min(Math.floor(s.timestampToMinute / size), mapTimeDiffLength - 1);
|
|
55
67
|
counts[index]++;
|
|
56
68
|
});
|
|
57
69
|
|
|
58
70
|
return counts.map((count, i) => ({
|
|
59
71
|
count,
|
|
60
72
|
level: 0,
|
|
61
|
-
description: `${
|
|
73
|
+
description: `${descriptions[i]}${count} ${type} submissions`,
|
|
62
74
|
}));
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
function
|
|
66
|
-
if (
|
|
77
|
+
function getHeatLevel(value: number, maxValue: number): number {
|
|
78
|
+
if (value <= 0 || maxValue <= 0) {
|
|
67
79
|
return 0;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const fraction = position - index;
|
|
73
|
-
|
|
74
|
-
if (sortedValues[index + 1] !== undefined) {
|
|
75
|
-
return sortedValues[index] + fraction * (sortedValues[index + 1] - sortedValues[index]);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return sortedValues[index];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function calcStaticThresholds(counts: number[]) {
|
|
82
|
-
const nonZeroCounts = counts.filter(c => c > 0);
|
|
83
|
-
|
|
84
|
-
if (nonZeroCounts.length === 0) {
|
|
85
|
-
return [0, 0, 0, 0, 0];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const sorted = [...nonZeroCounts].sort((a, b) => a - b);
|
|
89
|
-
|
|
90
|
-
return [
|
|
91
|
-
0,
|
|
92
|
-
calcPreciseQuan(sorted, 0.2),
|
|
93
|
-
calcPreciseQuan(sorted, 0.4),
|
|
94
|
-
calcPreciseQuan(sorted, 0.6),
|
|
95
|
-
calcPreciseQuan(sorted, 0.8),
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getDynamicHeatLevel(value: number, thresholds: number[]): number {
|
|
100
|
-
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
101
|
-
if (value === 0) {
|
|
102
|
-
return 0;
|
|
103
|
-
} else if (value >= thresholds[i]) {
|
|
104
|
-
return i;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return 0;
|
|
82
|
+
const level = Math.ceil((value / maxValue) * 4);
|
|
83
|
+
return Math.min(Math.max(level, 1), 4);
|
|
108
84
|
}
|
|
109
85
|
|
|
110
86
|
const heatMapData = computed(() =>
|
|
@@ -115,13 +91,13 @@ const heatMapData = computed(() =>
|
|
|
115
91
|
const incorrectHeatMap = generateHeatMap(incorrect, "incorrect");
|
|
116
92
|
|
|
117
93
|
const correctCounts = correctHeatMap.map(i => i.count);
|
|
118
|
-
const
|
|
94
|
+
const correctMax = Math.max(0, ...correctCounts);
|
|
119
95
|
|
|
120
96
|
const incorrectCounts = incorrectHeatMap.map(i => i.count);
|
|
121
|
-
const
|
|
97
|
+
const incorrectMax = Math.max(0, ...incorrectCounts);
|
|
122
98
|
|
|
123
|
-
correctHeatMap.forEach(i => i.level =
|
|
124
|
-
incorrectHeatMap.forEach(i => i.level =
|
|
99
|
+
correctHeatMap.forEach(i => i.level = getHeatLevel(i.count, correctMax));
|
|
100
|
+
incorrectHeatMap.forEach(i => i.level = getHeatLevel(i.count, incorrectMax));
|
|
125
101
|
|
|
126
102
|
return {
|
|
127
103
|
label: p.label,
|
|
@@ -145,14 +121,14 @@ const heatMapData = computed(() =>
|
|
|
145
121
|
</span>
|
|
146
122
|
|
|
147
123
|
<div
|
|
148
|
-
grid grid-cols-
|
|
149
|
-
gap-
|
|
124
|
+
grid grid-cols-4
|
|
125
|
+
gap-6 space-y-0
|
|
150
126
|
place-items-center
|
|
151
127
|
>
|
|
152
128
|
<div
|
|
153
129
|
v-for="heatMap in heatMapData" :key="heatMap.label"
|
|
154
|
-
class="w-
|
|
155
|
-
flex flex-col
|
|
130
|
+
class="w-280px h-100px"
|
|
131
|
+
flex flex-col gap-2
|
|
156
132
|
items-center justify-center
|
|
157
133
|
border border-gray-100 dark:border-gray-600
|
|
158
134
|
rounded-md
|
|
@@ -173,54 +149,48 @@ const heatMapData = computed(() =>
|
|
|
173
149
|
|
|
174
150
|
<div
|
|
175
151
|
flex flex-row
|
|
176
|
-
gap-
|
|
152
|
+
gap-0.5
|
|
177
153
|
>
|
|
178
154
|
<div
|
|
179
155
|
v-for="(correctItem, index) in heatMap.correctHeatMap" :key="`correct-${index}`"
|
|
180
156
|
>
|
|
181
|
-
<
|
|
182
|
-
:content="correctItem.description"
|
|
183
|
-
position="top"
|
|
184
|
-
>
|
|
157
|
+
<Tooltip>
|
|
185
158
|
<div
|
|
186
159
|
:data-level="correctItem.level"
|
|
187
160
|
class="correct-heat"
|
|
188
|
-
size-
|
|
189
|
-
rounded-
|
|
190
|
-
|
|
191
|
-
border border-gray-100 dark:border-gray-600
|
|
161
|
+
size-10px
|
|
162
|
+
rounded-0.7
|
|
163
|
+
bg-gray-200 dark:bg-gray-700
|
|
192
164
|
/>
|
|
193
|
-
|
|
165
|
+
<template #popper>
|
|
166
|
+
{{ correctItem.description }}
|
|
167
|
+
</template>
|
|
168
|
+
</Tooltip>
|
|
194
169
|
</div>
|
|
195
170
|
</div>
|
|
196
171
|
|
|
197
172
|
<div
|
|
198
173
|
flex flex-row
|
|
199
|
-
gap-
|
|
174
|
+
gap-0.5
|
|
200
175
|
>
|
|
201
176
|
<div
|
|
202
177
|
v-for="(incorrectItem, index) in heatMap.incorrectHeatMap" :key="`incorrect-${index}`"
|
|
203
178
|
>
|
|
204
|
-
<
|
|
205
|
-
:content="incorrectItem.description"
|
|
206
|
-
position="top"
|
|
207
|
-
>
|
|
179
|
+
<Tooltip>
|
|
208
180
|
<div
|
|
209
181
|
:data-level="incorrectItem.level"
|
|
210
182
|
class="incorrect-heat"
|
|
211
|
-
size-
|
|
212
|
-
rounded-
|
|
213
|
-
|
|
214
|
-
border border-gray-100 dark:border-gray-600
|
|
183
|
+
size-10px
|
|
184
|
+
rounded-0.7
|
|
185
|
+
bg-gray-200 dark:bg-gray-700
|
|
215
186
|
/>
|
|
216
|
-
|
|
187
|
+
<template #popper>
|
|
188
|
+
{{ incorrectItem.description }}
|
|
189
|
+
</template>
|
|
190
|
+
</Tooltip>
|
|
217
191
|
</div>
|
|
218
192
|
</div>
|
|
219
193
|
</div>
|
|
220
194
|
</div>
|
|
221
195
|
</div>
|
|
222
196
|
</template>
|
|
223
|
-
|
|
224
|
-
<style scoped lang="less">
|
|
225
|
-
@import "./SubmitHeatMap.less";
|
|
226
|
-
</style>
|
|
@@ -61,7 +61,7 @@ const isGirl = computed(() => team.value.isGirl);
|
|
|
61
61
|
class="h-12 w-12"
|
|
62
62
|
>
|
|
63
63
|
<div flex="1 ~ col" min-w-0>
|
|
64
|
-
<
|
|
64
|
+
<Tooltip placement="top">
|
|
65
65
|
<div
|
|
66
66
|
text="lg gray-900 dark:white"
|
|
67
67
|
font="semibold"
|
|
@@ -71,23 +71,23 @@ const isGirl = computed(() => team.value.isGirl);
|
|
|
71
71
|
<span v-if="isUnofficial" class="i-line-md:star-alt-filled" text="sm" />
|
|
72
72
|
<span v-if="isGirl" class="i-tabler:gender-female" text="sm pink-500" />
|
|
73
73
|
</div>
|
|
74
|
-
<template #
|
|
74
|
+
<template #popper>
|
|
75
75
|
<div>{{ t("team_info.team_id") }}{{ t("common.colon") }}{{ team.id }}</div>
|
|
76
76
|
<div>{{ t("team_info.team_name") }}{{ t("common.colon") }}{{ teamName }}</div>
|
|
77
77
|
</template>
|
|
78
|
-
</
|
|
79
|
-
<
|
|
78
|
+
</Tooltip>
|
|
79
|
+
<Tooltip v-if="organizationName" placement="top">
|
|
80
80
|
<div
|
|
81
81
|
text="sm gray-500 dark:gray-400"
|
|
82
82
|
class="line-clamp-1"
|
|
83
83
|
>
|
|
84
84
|
{{ organizationName }}
|
|
85
85
|
</div>
|
|
86
|
-
<template #
|
|
86
|
+
<template #popper>
|
|
87
87
|
<div>{{ t("team_info.org_id") }}{{ t("common.colon") }}{{ team.organization?.id }}</div>
|
|
88
88
|
<div>{{ t("team_info.org_name") }}{{ t("common.colon") }}{{ organizationName }}</div>
|
|
89
89
|
</template>
|
|
90
|
-
</
|
|
90
|
+
</Tooltip>
|
|
91
91
|
</div>
|
|
92
92
|
</div>
|
|
93
93
|
|
|
@@ -92,6 +92,7 @@ const tableRows = computed(() => {
|
|
|
92
92
|
const rows = [
|
|
93
93
|
{ label: t("team_info.rank"), value: team.value.rank, show: true },
|
|
94
94
|
{ label: t("team_info.team_name"), value: team.value.name.getOrDefault(lang.value), show: true },
|
|
95
|
+
{ label: t("team_info.description"), value: team.value.description?.getOrDefault(lang.value), show: !!team.value.description },
|
|
95
96
|
{ label: t("team_info.organization"), value: team.value.organization?.name.getOrDefault(lang.value), show: !!team.value.organization },
|
|
96
97
|
{ label: t("team_info.group"), value: groupNames.value, show: team.value.group.length > 0 },
|
|
97
98
|
{ label: t("team_info.members"), value: team.value.membersToString(lang.value), show: team.value.members.length > 0 },
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
export interface SegmentedControlOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
options: SegmentedControlOption[];
|
|
9
|
+
modelValue: string;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
(e: "update:modelValue", value: string): void;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
function selectOption(value: string) {
|
|
17
|
+
emit("update:modelValue", value);
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div
|
|
23
|
+
class="segmented-control"
|
|
24
|
+
inline-flex
|
|
25
|
+
rounded-lg
|
|
26
|
+
bg-gray-100
|
|
27
|
+
dark:bg-gray-800
|
|
28
|
+
p-1
|
|
29
|
+
border
|
|
30
|
+
border-gray-200
|
|
31
|
+
dark:border-gray-700
|
|
32
|
+
>
|
|
33
|
+
<button
|
|
34
|
+
v-for="option in options"
|
|
35
|
+
:key="option.value"
|
|
36
|
+
type="button"
|
|
37
|
+
class="segment-btn"
|
|
38
|
+
:class="{ active: modelValue === option.value }"
|
|
39
|
+
px-4
|
|
40
|
+
py-2
|
|
41
|
+
text-sm
|
|
42
|
+
font-medium
|
|
43
|
+
rounded-md
|
|
44
|
+
transition-all
|
|
45
|
+
duration-200
|
|
46
|
+
cursor-pointer
|
|
47
|
+
@click="selectOption(option.value)"
|
|
48
|
+
>
|
|
49
|
+
{{ option.label }}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<style scoped>
|
|
55
|
+
.segment-btn {
|
|
56
|
+
color: #6b7280;
|
|
57
|
+
background-color: transparent;
|
|
58
|
+
border: none;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.segment-btn:hover:not(.active) {
|
|
63
|
+
color: #374151;
|
|
64
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.segment-btn.active {
|
|
68
|
+
background-color: white;
|
|
69
|
+
color: #1e40af;
|
|
70
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.dark .segment-btn {
|
|
74
|
+
color: #9ca3af;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dark .segment-btn:hover:not(.active) {
|
|
78
|
+
color: #e5e7eb;
|
|
79
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dark .segment-btn.active {
|
|
83
|
+
background-color: #3b82f6;
|
|
84
|
+
color: #ffffff;
|
|
85
|
+
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -1,32 +1,98 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { TooltipInterface, TooltipOptions } from "flowbite";
|
|
3
3
|
import { Tooltip } from "flowbite";
|
|
4
|
-
|
|
5
4
|
import "./Tooltip.css";
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{
|
|
8
7
|
placement?: "left" | "right" | "top" | "bottom" | "auto";
|
|
9
8
|
}>();
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
10
|
+
const instanceId = `vue-tooltip-${getCurrentInstance()?.uid ?? Math.random().toString(36).slice(2)}`;
|
|
11
|
+
const tooltipTargetEl = ref<HTMLElement | null>(null);
|
|
12
|
+
const tooltipTriggerEl = ref<HTMLElement | null>(null);
|
|
13
|
+
const isTriggerVisible = useElementVisibility(tooltipTriggerEl);
|
|
14
|
+
const placement = computed(() => {
|
|
15
|
+
return props.placement ?? "auto";
|
|
16
|
+
});
|
|
13
17
|
|
|
14
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
15
18
|
let tooltip: TooltipInterface | null = null;
|
|
19
|
+
let cleanup: (() => void) | null = null;
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
function createTooltip() {
|
|
22
|
+
if (tooltip) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!tooltipTargetEl.value || !tooltipTriggerEl.value) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options: TooltipOptions = {
|
|
31
|
+
placement: placement.value as unknown as undefined,
|
|
32
|
+
triggerType: "none",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
tooltip = new Tooltip(tooltipTargetEl.value, tooltipTriggerEl.value, options, {
|
|
36
|
+
id: instanceId,
|
|
37
|
+
override: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function destroyTooltip() {
|
|
42
|
+
tooltip?.destroyAndRemoveInstance();
|
|
43
|
+
tooltip = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showTooltip() {
|
|
47
|
+
createTooltip();
|
|
48
|
+
tooltip?.show();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hideTooltip() {
|
|
52
|
+
tooltip?.hide();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(isTriggerVisible, (visible) => {
|
|
56
|
+
if (visible) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!tooltip) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
tooltip.hide();
|
|
65
|
+
destroyTooltip();
|
|
19
66
|
});
|
|
20
67
|
|
|
21
68
|
onMounted(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
triggerType: "hover",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
tooltip = new Tooltip(tooltipTargetEl.value, tooltipTriggerEl.value, options);
|
|
69
|
+
const triggerEl = tooltipTriggerEl.value;
|
|
70
|
+
if (!triggerEl) {
|
|
71
|
+
return;
|
|
29
72
|
}
|
|
73
|
+
|
|
74
|
+
const onMouseEnter = () => showTooltip();
|
|
75
|
+
const onMouseLeave = () => hideTooltip();
|
|
76
|
+
const onFocus = () => showTooltip();
|
|
77
|
+
const onBlur = () => hideTooltip();
|
|
78
|
+
|
|
79
|
+
triggerEl.addEventListener("mouseenter", onMouseEnter);
|
|
80
|
+
triggerEl.addEventListener("mouseleave", onMouseLeave);
|
|
81
|
+
triggerEl.addEventListener("focus", onFocus, true);
|
|
82
|
+
triggerEl.addEventListener("blur", onBlur, true);
|
|
83
|
+
|
|
84
|
+
cleanup = () => {
|
|
85
|
+
triggerEl.removeEventListener("mouseenter", onMouseEnter);
|
|
86
|
+
triggerEl.removeEventListener("mouseleave", onMouseLeave);
|
|
87
|
+
triggerEl.removeEventListener("focus", onFocus, true);
|
|
88
|
+
triggerEl.removeEventListener("blur", onBlur, true);
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onBeforeUnmount(() => {
|
|
93
|
+
cleanup?.();
|
|
94
|
+
cleanup = null;
|
|
95
|
+
destroyTooltip();
|
|
30
96
|
});
|
|
31
97
|
</script>
|
|
32
98
|
|
package/src/components.d.ts
CHANGED
|
@@ -34,7 +34,6 @@ declare module 'vue' {
|
|
|
34
34
|
GiantsOptions: typeof import('./components/battle-of-giants/GiantsOptions.vue')['default']
|
|
35
35
|
GiantsScoreBoard: typeof import('./components/battle-of-giants/GiantsScoreBoard.vue')['default']
|
|
36
36
|
GoBack: typeof import('./components/GoBack.vue')['default']
|
|
37
|
-
HeatMapTooltip: typeof import('./components/board/HeatMapTooltip.vue')['default']
|
|
38
37
|
InfoModal: typeof import('./components/board/InfoModal.vue')['default']
|
|
39
38
|
Modal: typeof import('./components/board/Modal.vue')['default']
|
|
40
39
|
ModalMenu: typeof import('./components/board/ModalMenu.vue')['default']
|
|
@@ -62,7 +61,9 @@ declare module 'vue' {
|
|
|
62
61
|
RouterLink: typeof import('vue-router')['RouterLink']
|
|
63
62
|
RouterView: typeof import('vue-router')['RouterView']
|
|
64
63
|
SearchInput: typeof import('./components/SearchInput.vue')['default']
|
|
64
|
+
SeatMapView: typeof import('./components/board/SeatMapView.vue')['default']
|
|
65
65
|
SecondLevelMenu: typeof import('./components/board/SecondLevelMenu.vue')['default']
|
|
66
|
+
SegmentedControl: typeof import('./components/common/SegmentedControl.vue')['default']
|
|
66
67
|
Standings: typeof import('./components/board/Standings.vue')['default']
|
|
67
68
|
Statistics: typeof import('./components/board/Statistics.vue')['default']
|
|
68
69
|
SubmissionsTable: typeof import('./components/board/SubmissionsTable.vue')['default']
|
|
@@ -28,6 +28,23 @@ async function fetchAndAssignOrganizations(contest: Contest, baseUrl: string): P
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
async function fetchAndAssignSeatMap(contest: Contest, baseUrl: string): Promise<void> {
|
|
32
|
+
if (!contest?.seat_map || !isDataItem(contest.seat_map)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { url } = contest.seat_map;
|
|
37
|
+
let fetchUrl = url;
|
|
38
|
+
if (!url.startsWith("http")) {
|
|
39
|
+
fetchUrl = `${baseUrl}/${url}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const seatMapRes = await fetch(fetchUrl);
|
|
43
|
+
if (seatMapRes.ok) {
|
|
44
|
+
contest.seat_map = await seatMapRes.json();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
async function fetch_board_data(target: string): Promise<BoardData> {
|
|
32
49
|
const endpoint = target.startsWith("/") ? target.slice(1) : target;
|
|
33
50
|
let prefix = `${window.DATA_HOST}${endpoint}`;
|
|
@@ -68,6 +85,7 @@ async function fetch_board_data(target: string): Promise<BoardData> {
|
|
|
68
85
|
|
|
69
86
|
const contest = jsonData[0];
|
|
70
87
|
await fetchAndAssignOrganizations(contest, prefix);
|
|
88
|
+
await fetchAndAssignSeatMap(contest, prefix);
|
|
71
89
|
|
|
72
90
|
return {
|
|
73
91
|
contest,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as w,m as L,f as C,c as n,o as s,p as i,q as D,u as e,n as O,h as c,b as o,I as k,t as d,A as F,C as I,F as N,E as V,G as z}from"./index-B0gZ0sxU.js";import{B}from"./constant-C2gH6ZII.js";import{c as R,a as U,R as A,B as q}from"./rank-BUkksYCC.js";import{c as G}from"./use-vmodel-Cl0DXIYt.js";import{u as M}from"./TheInput.vue_vue_type_script_setup_true_lang-DPzm-iVE.js";const Q={flex:"","flex-1":"","flex-col":"","justify-center":"","items-start":""},X={class:"resolver-team-name",truncate:"","overflow-hidden":""},H={"w-32":"",flex:"","flex-shrink-0":"","flex-row":"","justify-start":"","items-center":""},J=w({__name:"BalloonBlock",props:{index:null,balloon:null},setup(p){const f=p,b=c(()=>f.index),r=c(()=>f.balloon),v=i(null),y=L(v),{locale:_}=C(),l=c(()=>_.value);function x(a){return[a.location,a.organization?.name.getOrDefault(l.value),a.name.getOrDefault(l.value)].filter(t=>t).join(" - ")}const u=c(()=>r.value.problem.balloonColor);return(a,m)=>(s(),n("div",{ref_key:"el",ref:v,"h-24":""},[e(y)?(s(),n("div",{key:0,"h-24":"",flex:"","flex-row":"","gap-x-4":"","font-mono":"","text-4xl":"",class:O([e(b)%2===0?"bg-resolver-bg-0":"bg-resolver-bg-1"])},[o("div",{"w-20":"",flex:"","flex-shrink-0":"","justify-center":"","items-center":"",style:k({backgroundColor:e(u).background_color})},[o("div",{style:k({color:e(u).color})},d(e(r).problem.label),5)],4),o("div",Q,[o("div",X,d(x(e(r).team)),1),m[0]||(m[0]=o("div",{flex:"","flex-row":"","text-sm":"","items-start":"","gap-x-2":""},null,-1))]),o("div",H,d(e(r).submission.timestampToMinute),1)],2)):D("",!0)],512))}}),K={class:"bg-[#323443]","text-gray-200":"","w-screen":"","h-screen":""},P={key:0},W={flex:"","flex-col":"","justify-center":"","items-center":"","w-screen":"","h-screen":"","text-xl":"",italic:""},Y={key:0},Z={key:1},ee={flex:"","flex-col":"","justify-between":""},re=w({__name:"Balloon",props:{dataSourceUrl:null},setup(p){const f=p,b=F(B),{t:r,locale:v}=C(),y=c(()=>v.value),_=i(!1),l=i({}),x=i([]),u=i([]),a=i({});function m(){const g=new A(l.value,x.value,u.value);g.buildBalloons(),a.value=g}const{data:t,isError:j,error:S}=M(f.dataSourceUrl);I(t,async()=>{t.value===null||t.value===void 0||(l.value=G(t.value?.contest),b.value=`${l.value.name.getOrDefault(y.value)} | ${B}`,x.value=R(t.value?.teams),u.value=U(t.value?.submissions,l.value),m(),_.value=!0)},{immediate:!0});const T=c(()=>a.value.balloons.sort(q.compare).reverse().slice(0,256));return(g,oe)=>{const $=J;return s(),n("div",K,[e(_)?(s(),n("div",Z,[o("div",ee,[(s(!0),n(N,null,V(e(T),(h,E)=>(s(),z($,{key:h.key,index:E,balloon:h},null,8,["index","balloon"]))),128))])])):(s(),n("div",P,[o("div",W,[o("div",null,d(e(r)("common.loading"))+"... ",1),e(j)?(s(),n("div",Y,d(e(S)),1)):D("",!0)])]))])}}});export{re as _};
|