batch update
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-10-08 00:46:49 +08:00
parent b8c622dde1
commit b14316b919
48 changed files with 1236 additions and 735 deletions

View File

@@ -1,211 +1,228 @@
import confetti from "canvas-confetti"
/**
* 随机烟花效果 Composable
* 提供7种不同风格的烟花庆祝效果
*/
export function useFireworks() {
/**
* 触发随机烟花效果
*/
function celebrate() {
const fireworkTypes = [
// 效果1: 经典烟花秀
() => {
const duration = 3000
const animationEnd = Date.now() + duration
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 }
const interval: any = setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) return clearInterval(interval)
const particleCount = 50 * (timeLeft / duration)
confetti({
...defaults,
particleCount,
origin: { x: Math.random() * 0.3 + 0.1, y: Math.random() - 0.2 },
colors: ["#ff6b6b", "#ffd93d", "#6bcf7f", "#4ecdc4", "#a29bfe"],
})
confetti({
...defaults,
particleCount,
origin: { x: Math.random() * 0.3 + 0.7, y: Math.random() - 0.2 },
colors: ["#ff6b6b", "#ffd93d", "#6bcf7f", "#4ecdc4", "#a29bfe"],
})
}, 250)
},
// 效果2: 星星雨
() => {
const count = 10
const defaults = {
origin: { y: 0.7 },
shapes: ["star"],
colors: ["#FFD700", "#FFA500", "#FFFF00", "#FF69B4", "#00CED1"],
}
function fire(particleRatio: number, opts: any) {
confetti({ ...defaults, ...opts, particleCount: Math.floor(200 * particleRatio) })
}
fire(0.25, { spread: 26, startVelocity: 55 })
fire(0.2, { spread: 60 })
fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 })
fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 })
fire(0.1, { spread: 120, startVelocity: 45 })
},
// 效果3: 爆炸波浪
() => {
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min
}
for (let i = 0; i < 5; i++) {
setTimeout(() => {
confetti({
angle: randomInRange(55, 125),
spread: randomInRange(50, 70),
particleCount: randomInRange(50, 100),
origin: { y: 0.6 },
colors: ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"],
})
}, i * 200)
}
},
// 效果4: 彩虹喷泉
() => {
const end = Date.now() + 2000
const colors = ["#bb0000", "#ffffff"]
const frame = () => {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: { x: 0 },
colors: colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: { x: 1 },
colors: colors,
})
if (Date.now() < end) {
requestAnimationFrame(frame)
}
}
frame()
},
// 效果5: 烟花雨
() => {
const duration = 2500
const animationEnd = Date.now() + duration
const interval: any = setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) return clearInterval(interval)
const particleCount = 50
confetti({
particleCount,
startVelocity: 30,
spread: 360,
ticks: 60,
origin: {
x: Math.random(),
y: Math.random() - 0.2,
},
colors: ["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff"],
})
}, 200)
},
// 效果6: 炮竹齐鸣
() => {
const count = 200
const defaults = {
origin: { y: 0.7 },
}
function fire(particleRatio: number, opts: any) {
confetti({
...defaults,
...opts,
particleCount: Math.floor(count * particleRatio),
})
}
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
},
// 效果7: 螺旋上升
() => {
const defaults = {
spread: 360,
ticks: 100,
gravity: 0,
decay: 0.94,
startVelocity: 30,
}
function shoot() {
confetti({
...defaults,
particleCount: 50,
scalar: 1.2,
shapes: ["circle", "square"],
colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"],
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
setTimeout(shoot, 300)
setTimeout(shoot, 400)
},
]
// 随机选择一种效果
const randomEffect = fireworkTypes[Math.floor(Math.random() * fireworkTypes.length)]
randomEffect()
}
return {
celebrate,
}
}
import confetti from "canvas-confetti"
/**
* 随机烟花效果 Composable
* 提供7种不同风格的烟花庆祝效果
*/
export function useFireworks() {
/**
* 触发随机烟花效果
*/
function celebrate() {
const fireworkTypes = [
// 效果1: 经典烟花秀
() => {
const duration = 3000
const animationEnd = Date.now() + duration
const defaults = {
startVelocity: 30,
spread: 360,
ticks: 60,
zIndex: 0,
}
const interval: any = setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) return clearInterval(interval)
const particleCount = 50 * (timeLeft / duration)
confetti({
...defaults,
particleCount,
origin: { x: Math.random() * 0.3 + 0.1, y: Math.random() - 0.2 },
colors: ["#ff6b6b", "#ffd93d", "#6bcf7f", "#4ecdc4", "#a29bfe"],
})
confetti({
...defaults,
particleCount,
origin: { x: Math.random() * 0.3 + 0.7, y: Math.random() - 0.2 },
colors: ["#ff6b6b", "#ffd93d", "#6bcf7f", "#4ecdc4", "#a29bfe"],
})
}, 250)
},
// 效果2: 星星雨
() => {
const count = 10
const defaults = {
origin: { y: 0.7 },
shapes: ["star"],
colors: ["#FFD700", "#FFA500", "#FFFF00", "#FF69B4", "#00CED1"],
}
function fire(particleRatio: number, opts: any) {
confetti({
...defaults,
...opts,
particleCount: Math.floor(200 * particleRatio),
})
}
fire(0.25, { spread: 26, startVelocity: 55 })
fire(0.2, { spread: 60 })
fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 })
fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 })
fire(0.1, { spread: 120, startVelocity: 45 })
},
// 效果3: 爆炸波浪
() => {
function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min
}
for (let i = 0; i < 5; i++) {
setTimeout(() => {
confetti({
angle: randomInRange(55, 125),
spread: randomInRange(50, 70),
particleCount: randomInRange(50, 100),
origin: { y: 0.6 },
colors: ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"],
})
}, i * 200)
}
},
// 效果4: 彩虹喷泉
() => {
const end = Date.now() + 2000
const colors = ["#bb0000", "#ffffff"]
const frame = () => {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: { x: 0 },
colors: colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: { x: 1 },
colors: colors,
})
if (Date.now() < end) {
requestAnimationFrame(frame)
}
}
frame()
},
// 效果5: 烟花雨
() => {
const duration = 2500
const animationEnd = Date.now() + duration
const interval: any = setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) return clearInterval(interval)
const particleCount = 50
confetti({
particleCount,
startVelocity: 30,
spread: 360,
ticks: 60,
origin: {
x: Math.random(),
y: Math.random() - 0.2,
},
colors: [
"#ff0000",
"#00ff00",
"#0000ff",
"#ffff00",
"#ff00ff",
"#00ffff",
],
})
}, 200)
},
// 效果6: 炮竹齐鸣
() => {
const count = 200
const defaults = {
origin: { y: 0.7 },
}
function fire(particleRatio: number, opts: any) {
confetti({
...defaults,
...opts,
particleCount: Math.floor(count * particleRatio),
})
}
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
},
// 效果7: 螺旋上升
() => {
const defaults = {
spread: 360,
ticks: 100,
gravity: 0,
decay: 0.94,
startVelocity: 30,
}
function shoot() {
confetti({
...defaults,
particleCount: 50,
scalar: 1.2,
shapes: ["circle", "square"],
colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"],
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
setTimeout(shoot, 300)
setTimeout(shoot, 400)
},
]
// 随机选择一种效果
const randomEffect =
fireworkTypes[Math.floor(Math.random() * fireworkTypes.length)]
randomEffect()
}
return {
celebrate,
}
}

View File

@@ -1,172 +1,172 @@
import { ref } from "vue"
import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { Submission } from "utils/types"
import {
useSubmissionWebSocket,
type SubmissionUpdate,
} from "shared/composables/websocket"
/**
* 判题监控 Composable
* 负责通过 WebSocket + 轮询双保险机制监控判题结果
*/
export function useSubmissionMonitor() {
// ==================== 状态 ====================
const submissionId = ref("")
const submission = ref<Submission>()
// ==================== 轮询机制 ====================
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
async () => {
if (!submissionId.value) return
try {
const res = await getSubmission(submissionId.value)
submission.value = res.data
const result = res.data.result
// 判题完成,停止轮询
if (
result !== SubmissionStatus.judging &&
result !== SubmissionStatus.pending
) {
pausePolling()
}
} catch (error) {
console.error("[SubmissionMonitor] 轮询失败:", error)
pausePolling()
}
},
2000,
{ immediate: false }
)
// ==================== WebSocket 处理 ====================
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
if (data.submission_id !== submissionId.value) {
console.log("[SubmissionMonitor] 提交ID不匹配忽略")
return
}
if (!submission.value) {
submission.value = {} as Submission
}
submission.value.result = data.result as Submission["result"]
// 判题完成或出错,获取完整详情
if (data.status === "finished" || data.status === "error") {
console.log(
`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`
)
// 停止轮询WebSocket已成功
pausePolling()
getSubmission(submissionId.value).then((res) => {
submission.value = res.data
// 15分钟无新提交则断开WebSocket节省资源
scheduleDisconnect(15 * 60 * 1000)
})
}
}
// 初始化 WebSocket
const {
connect,
subscribe,
scheduleDisconnect,
cancelScheduledDisconnect,
status: wsStatus,
} = useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询保底启动 ====================
const { start: startPollingFallback } = useTimeoutFn(
() => {
if (
submission.value &&
(submission.value.result === SubmissionStatus.judging ||
submission.value.result === SubmissionStatus.pending ||
submission.value.result === 9) // 9 = submitting
) {
console.log("[SubmissionMonitor] WebSocket未及时响应启动轮询保底")
resumePolling()
}
},
5000,
{ immediate: false }
)
// ==================== 启动监控 ====================
const startMonitoring = (id: string) => {
submissionId.value = id
submission.value = { result: 9 } as Submission // 9 = submitting
// 取消之前的断开计划
cancelScheduledDisconnect()
// 如果WebSocket未连接先连接
if (wsStatus.value !== "connected") {
console.log("[SubmissionMonitor] 启动WebSocket连接...")
connect()
}
// 等待WebSocket连接并订阅
const unwatch = watch(
wsStatus,
(status) => {
if (status === "connected") {
console.log("[SubmissionMonitor] WebSocket已连接订阅提交:", id)
subscribe(id)
unwatch() // 订阅成功后停止监听
}
},
{ immediate: true }
)
// 5秒后启动轮询保底防止WebSocket失败
startPollingFallback()
}
// ==================== 计算属性 ====================
const judging = computed(
() => submission.value?.result === SubmissionStatus.judging
)
const pending = computed(
() => submission.value?.result === SubmissionStatus.pending
)
const submitting = computed(
() => submission.value?.result === SubmissionStatus.submitting
)
const isProcessing = computed(() => {
return judging.value || pending.value || submitting.value
})
// ==================== 清理 ====================
onUnmounted(() => {
pausePolling()
})
return {
// 状态
submissionId,
submission,
// 计算属性
judging,
pending,
submitting,
isProcessing,
// 方法
startMonitoring,
pausePolling,
}
}
import { ref, computed, watch, onUnmounted } from "vue"
import { useIntervalFn, useTimeoutFn } from "@vueuse/core"
import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { Submission } from "utils/types"
import {
useSubmissionWebSocket,
type SubmissionUpdate,
} from "shared/composables/websocket"
/**
* 判题监控 Composable
* 负责通过 WebSocket + 轮询双保险机制监控判题结果
*/
export function useSubmissionMonitor() {
// ==================== 状态 ====================
const submissionId = ref("")
const submission = ref<Submission>()
// ==================== 轮询机制 ====================
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
async () => {
if (!submissionId.value) return
try {
const res = await getSubmission(submissionId.value)
submission.value = res.data
const result = res.data.result
// 判题完成,停止轮询
if (
result !== SubmissionStatus.judging &&
result !== SubmissionStatus.pending
) {
pausePolling()
}
} catch (error) {
console.error("[SubmissionMonitor] 轮询失败:", error)
pausePolling()
}
},
2000,
{ immediate: false },
)
// ==================== WebSocket 处理 ====================
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
if (data.submission_id !== submissionId.value) {
console.log("[SubmissionMonitor] 提交ID不匹配忽略")
return
}
if (!submission.value) {
submission.value = {} as Submission
}
submission.value.result = data.result as Submission["result"]
// 判题完成或出错,获取完整详情
if (data.status === "finished" || data.status === "error") {
console.log(
`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`,
)
// 停止轮询WebSocket已成功
pausePolling()
getSubmission(submissionId.value).then((res) => {
submission.value = res.data
// 15分钟无新提交则断开WebSocket节省资源
scheduleDisconnect(15 * 60 * 1000)
})
}
}
// 初始化 WebSocket
const {
connect,
subscribe,
scheduleDisconnect,
cancelScheduledDisconnect,
status: wsStatus,
} = useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询保底启动 ====================
const { start: startPollingFallback } = useTimeoutFn(
() => {
if (
submission.value &&
(submission.value.result === SubmissionStatus.judging ||
submission.value.result === SubmissionStatus.pending ||
submission.value.result === 9) // 9 = submitting
) {
console.log("[SubmissionMonitor] WebSocket未及时响应启动轮询保底")
resumePolling()
}
},
5000,
{ immediate: false },
)
// ==================== 启动监控 ====================
const startMonitoring = (id: string) => {
submissionId.value = id
submission.value = { result: 9 } as Submission // 9 = submitting
// 取消之前的断开计划
cancelScheduledDisconnect()
// 如果WebSocket未连接先连接
if (wsStatus.value !== "connected") {
console.log("[SubmissionMonitor] 启动WebSocket连接...")
connect()
}
// 等待WebSocket连接并订阅
const unwatch = watch(
wsStatus,
(status) => {
if (status === "connected") {
console.log("[SubmissionMonitor] WebSocket已连接订阅提交:", id)
subscribe(id)
unwatch() // 订阅成功后停止监听
}
},
{ immediate: true },
)
// 5秒后启动轮询保底防止WebSocket失败
startPollingFallback()
}
// ==================== 计算属性 ====================
const judging = computed(
() => submission.value?.result === SubmissionStatus.judging,
)
const pending = computed(
() => submission.value?.result === SubmissionStatus.pending,
)
const submitting = computed(
() => submission.value?.result === SubmissionStatus.submitting,
)
const isProcessing = computed(() => {
return judging.value || pending.value || submitting.value
})
// ==================== 清理 ====================
onUnmounted(() => {
pausePolling()
})
return {
// 状态
submissionId,
submission,
// 计算属性
judging,
pending,
submitting,
isProcessing,
// 方法
startMonitoring,
pausePolling,
}
}