Skip to content

Commit 638b893

Browse files
committed
feat: add submit deadline
1 parent 86142e6 commit 638b893

3 files changed

Lines changed: 167 additions & 4 deletions

File tree

src/utils/deadline.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Deadline utilities for submission cutoff
2+
// Deadline: 2025-09-03 13:00 Beijing Time (UTC+8) => 2025-09-03T05:00:00Z
3+
4+
export const DEADLINE_UTC_ISO = '2025-09-03T05:00:00Z';
5+
export const DEADLINE_LABEL = '2025年9月3日 北京时间 13:00';
6+
export const DEADLINE_TS = Date.parse(DEADLINE_UTC_ISO);
7+
8+
export const getRemainingMs = (now: number = Date.now()): number => {
9+
const diff = DEADLINE_TS - now;
10+
return diff > 0 ? diff : 0;
11+
};
12+
13+
export const isPastDeadline = (now: number = Date.now()): boolean => {
14+
return now >= DEADLINE_TS;
15+
};
16+
17+
export const formatCountdown = (ms: number): string => {
18+
if (ms <= 0) return '已截止';
19+
const totalSeconds = Math.floor(ms / 1000);
20+
const days = Math.floor(totalSeconds / 86400);
21+
const hours = Math.floor((totalSeconds % 86400) / 3600);
22+
const minutes = Math.floor((totalSeconds % 3600) / 60);
23+
const seconds = totalSeconds % 60;
24+
25+
const two = (n: number) => (n < 10 ? `0${n}` : String(n));
26+
const hms = `${two(hours)}:${two(minutes)}:${two(seconds)}`;
27+
return days > 0 ? `${days}${hms}` : hms;
28+
};
29+

src/views/Home.vue

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
<router-link to="/submit" class="menu-button">
2121
<div class="pixel-icon code-icon"></div>
2222
<span>提交算法</span>
23+
<span
24+
v-if="!isPastDeadline"
25+
class="menu-countdown-badge"
26+
:class="{ soon: isSoon && !isUrgent, urgent: isUrgent }"
27+
>
28+
剩余 {{ countdownText }}
29+
</span>
30+
<span v-else class="menu-countdown-badge ended">已截止</span>
2331
</router-link>
2432

2533
<router-link to="/tournament" class="menu-button tournament-button">
@@ -42,12 +50,36 @@
4250
</template>
4351

4452
<script setup lang="ts">
45-
import { computed } from 'vue';
53+
import { computed, ref, onMounted, onUnmounted } from 'vue';
4654
import { useAuth } from '../stores/auth';
55+
import { getRemainingMs, isPastDeadline as pastDeadlineFn, formatCountdown } from '../utils/deadline';
4756
4857
const { state } = useAuth();
4958
5059
const nickname = computed(() => state.user?.nickname || '玩家');
60+
61+
// 截止倒计时(首页展示)
62+
const nowMs = ref<number>(Date.now());
63+
const isPastDeadline = computed(() => pastDeadlineFn(nowMs.value));
64+
const remainingMs = computed(() => getRemainingMs(nowMs.value));
65+
const countdownText = computed(() => formatCountdown(remainingMs.value));
66+
const isUrgent = computed(() => remainingMs.value > 0 && remainingMs.value <= 60 * 60 * 1000); // < 1h
67+
const isSoon = computed(() => remainingMs.value > 0 && remainingMs.value <= 24 * 60 * 60 * 1000); // < 24h
68+
let deadlineTimer: number | null = null;
69+
70+
onMounted(() => {
71+
if (deadlineTimer) clearInterval(deadlineTimer);
72+
deadlineTimer = setInterval(() => {
73+
nowMs.value = Date.now();
74+
}, 1000) as unknown as number;
75+
});
76+
77+
onUnmounted(() => {
78+
if (deadlineTimer) {
79+
clearInterval(deadlineTimer);
80+
deadlineTimer = null;
81+
}
82+
});
5183
</script>
5284

5385
<style scoped>
@@ -125,6 +157,39 @@ const nickname = computed(() => state.user?.nickname || '玩家');
125157
font-family: 'Press Start 2P', monospace;
126158
}
127159
160+
/* 提交截止倒计时徽标 */
161+
.menu-countdown-badge {
162+
margin-left: auto;
163+
padding: 4px 6px;
164+
border: 2px solid var(--accent-color);
165+
color: var(--accent-color);
166+
background: rgba(74, 222, 128, 0.12);
167+
font-size: 12px;
168+
line-height: 1;
169+
border-radius: 4px;
170+
font-family: 'Press Start 2P', monospace;
171+
}
172+
.menu-countdown-badge.soon {
173+
border-color: #ffc107;
174+
color: #ffc107;
175+
background: rgba(255, 193, 7, 0.12);
176+
}
177+
.menu-countdown-badge.urgent {
178+
border-color: var(--error-color);
179+
color: var(--error-color);
180+
background: rgba(239, 68, 68, 0.12);
181+
animation: subtlePulse 1.4s ease-in-out infinite;
182+
}
183+
.menu-countdown-badge.ended {
184+
border-color: var(--error-color);
185+
color: var(--error-color);
186+
background: rgba(239, 68, 68, 0.12);
187+
}
188+
@keyframes subtlePulse {
189+
0%, 100% { transform: translateY(0); filter: brightness(1); }
190+
50% { transform: translateY(-1px); filter: brightness(1.1); }
191+
}
192+
128193
.menu-button:hover {
129194
border-color: var(--accent-color);
130195
transform: translateY(-2px);

src/views/SubmitCode.vue

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
<div class="pixel-snake"></div>
77
</div>
88

9+
<!-- 截止时间与倒计时提示 -->
10+
<div class="deadline-banner pixel-border" :class="{ passed: isPastDeadline }">
11+
<template v-if="!isPastDeadline">
12+
<span class="deadline-text">提交截止倒计时:</span>
13+
<span class="deadline-timer">{{ countdownText }}</span>
14+
<span class="deadline-note">(截止时间:{{ DEADLINE_LABEL }})</span>
15+
</template>
16+
<template v-else>
17+
<span class="deadline-text">提交已截止</span>
18+
<span class="deadline-note">(截止时间:{{ DEADLINE_LABEL }})</span>
19+
</template>
20+
</div>
21+
922
<div class="submit-content">
1023
<div v-if="!isCompilingUI && !hasResult" class="upload-section">
1124
<label for="codeFile" class="file-label pixel-border"
@@ -37,15 +50,16 @@
3750

3851
<div class="submit-actions">
3952
<button @click="handleSubmit" class="pixel-button"
40-
:disabled="isUploading || !selectedFile || rateLimitCountdown > 0">
41-
{{ isUploading ? '上传中...' : rateLimitCountdown > 0 ? `等待${rateLimitCountdown}秒` : '提交代码' }}
53+
:disabled="isUploading || !selectedFile || rateLimitCountdown > 0 || isPastDeadline">
54+
{{ isPastDeadline ? '已截止' : (isUploading ? '上传中...' : rateLimitCountdown > 0 ? `等待${rateLimitCountdown}秒` : '提交代码') }}
4255
</button>
4356
</div>
4457

4558
<!-- 显示所有上传阶段的错误(文件类型、大小、429等) -->
4659
<div v-if="error" :class="['error-message', { 'rate-limit-error': rateLimitCountdown > 0 }]">
4760
{{ rateLimitCountdown > 0 ? `请求频率过高,请${rateLimitCountdown}秒后重试` : error }}
4861
</div>
62+
<div v-if="isPastDeadline" class="deadline-hint">已超过截止时间,禁止提交。</div>
4963
</div>
5064

5165
<!-- 编译/结果区域:在上传完成后或 SUBMITTED 后进入显示(统一使用状态条样式) -->
@@ -102,11 +116,12 @@
102116
</template>
103117

104118
<script setup lang="ts">
105-
import { ref, onUnmounted, computed, nextTick } from 'vue';
119+
import { ref, onUnmounted, onMounted, computed, nextTick } from 'vue';
106120
import { sandboxService } from '../services/sandboxService';
107121
import { useAuth } from '../stores/auth';
108122
import { SSE } from 'sse.js';
109123
import { RateLimitError } from '../types/RateLimitError';
124+
import { DEADLINE_TS, DEADLINE_LABEL, getRemainingMs, isPastDeadline as pastDeadlineFn, formatCountdown } from '../utils/deadline';
110125
111126
const { state } = useAuth();
112127
@@ -125,6 +140,12 @@ const jobId = ref<string | null>(null);
125140
const rateLimitCountdown = ref<number>(0);
126141
const rateLimitTimer = ref<number | null>(null);
127142
143+
// 截止时间与倒计时
144+
const nowMs = ref<number>(Date.now());
145+
const isPastDeadline = computed(() => pastDeadlineFn(nowMs.value));
146+
const countdownText = computed(() => formatCountdown(getRemainingMs(nowMs.value)));
147+
let deadlineTimer: number | null = null;
148+
128149
const statusBarClass = computed(() => {
129150
if (hasResult.value && compilationStatus.value) {
130151
return `status-${compilationStatus.value}`;
@@ -361,6 +382,11 @@ const handleSubmit = async () => {
361382
return;
362383
}
363384
385+
if (isPastDeadline.value) {
386+
error.value = '已超过截止时间,无法提交';
387+
return;
388+
}
389+
364390
// 重置状态
365391
closeSSEConnection();
366392
isUploading.value = true;
@@ -429,9 +455,21 @@ const formatDate = (timestamp: number): string => {
429455
};
430456
431457
// 组件卸载时清理SSE连接和定时器
458+
onMounted(() => {
459+
// 倒计时更新
460+
if (deadlineTimer) clearInterval(deadlineTimer);
461+
deadlineTimer = setInterval(() => {
462+
nowMs.value = Date.now();
463+
}, 1000) as unknown as number;
464+
});
465+
432466
onUnmounted(() => {
433467
closeSSEConnection();
434468
clearRateLimitTimer();
469+
if (deadlineTimer) {
470+
clearInterval(deadlineTimer);
471+
deadlineTimer = null;
472+
}
435473
});
436474
</script>
437475

@@ -451,6 +489,37 @@ onUnmounted(() => {
451489
overflow-y: auto;
452490
}
453491
492+
/* 截止时间提示条 */
493+
.deadline-banner {
494+
display: flex;
495+
align-items: center;
496+
gap: 8px;
497+
padding: 10px 12px;
498+
margin-bottom: 16px;
499+
background: var(--input-bg);
500+
border: 2px solid var(--border-color);
501+
}
502+
.deadline-banner.passed {
503+
border-color: var(--error-color);
504+
background: rgba(239, 68, 68, 0.08);
505+
}
506+
.deadline-text {
507+
font-weight: 600;
508+
color: var(--text-color);
509+
}
510+
.deadline-timer {
511+
font-family: monospace;
512+
color: var(--accent-color);
513+
}
514+
.deadline-note {
515+
color: var(--border-color);
516+
font-size: 12px;
517+
}
518+
.deadline-hint {
519+
text-align: center;
520+
color: var(--error-color);
521+
}
522+
454523
/* 自定义滚动条样式 */
455524
.submit-card::-webkit-scrollbar {
456525
width: 12px;

0 commit comments

Comments
 (0)