feat(game/ui): 实现周报结算弹窗,包含10秒倒计时、黑话答题和奖励/惩罚逻辑
This commit is contained in:
285
game/ui/WeeklyReportModal.ts
Normal file
285
game/ui/WeeklyReportModal.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
import { generateWeeklyOptions } from '../data/buzzwords'
|
||||||
|
|
||||||
|
export type WeeklyReportCallbacks = {
|
||||||
|
onBossInspection: () => void
|
||||||
|
onFullStamina: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周报结算弹窗
|
||||||
|
* 每3波结束后触发,仿钉钉/飞书全屏通知风格
|
||||||
|
* 用 DOM 层实现(z-index: 9999),覆盖 canvas
|
||||||
|
*/
|
||||||
|
export class WeeklyReportModal {
|
||||||
|
private container: HTMLElement | null = null
|
||||||
|
private timerInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
private callbacks: WeeklyReportCallbacks
|
||||||
|
private onClose?: () => void
|
||||||
|
|
||||||
|
constructor(callbacks: WeeklyReportCallbacks) {
|
||||||
|
this.callbacks = callbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示弹窗 */
|
||||||
|
show(onClose?: () => void): void {
|
||||||
|
this.onClose = onClose
|
||||||
|
this.createDOM()
|
||||||
|
this.startTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDOM(): void {
|
||||||
|
// 移除旧弹窗(防止重复)
|
||||||
|
this.cleanup()
|
||||||
|
|
||||||
|
const { options, correctIndex } = generateWeeklyOptions()
|
||||||
|
|
||||||
|
// 背景蒙层
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.id = 'weekly-report-overlay'
|
||||||
|
overlay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.82);
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
animation: fadeInOverlay 0.3s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 弹窗卡片
|
||||||
|
const card = document.createElement('div')
|
||||||
|
card.style.cssText = `
|
||||||
|
background: #1a1a2e;
|
||||||
|
border: 2px solid #7c3aed;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px 40px;
|
||||||
|
max-width: 520px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 0 40px rgba(124,58,237,0.5);
|
||||||
|
position: relative;
|
||||||
|
animation: slideInCard 0.35s ease;
|
||||||
|
`
|
||||||
|
|
||||||
|
// 顶部进度条容器
|
||||||
|
const progressWrap = document.createElement('div')
|
||||||
|
progressWrap.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 6px;
|
||||||
|
background: #374151;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
const progressBar = document.createElement('div')
|
||||||
|
progressBar.id = 'weekly-progress-bar'
|
||||||
|
progressBar.style.cssText = `
|
||||||
|
height: 100%;
|
||||||
|
background: #ef4444;
|
||||||
|
width: 100%;
|
||||||
|
transition: width linear;
|
||||||
|
`
|
||||||
|
progressWrap.appendChild(progressBar)
|
||||||
|
card.appendChild(progressWrap)
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
const title = document.createElement('div')
|
||||||
|
title.textContent = '📊 周报提交时间!'
|
||||||
|
title.style.cssText = `
|
||||||
|
font-size: 28px;
|
||||||
|
color: #a78bfa;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
`
|
||||||
|
card.appendChild(title)
|
||||||
|
|
||||||
|
// 副标题
|
||||||
|
const subtitle = document.createElement('div')
|
||||||
|
subtitle.textContent = '请选择正确的互联网黑话,提交本周工作总结'
|
||||||
|
subtitle.style.cssText = `
|
||||||
|
font-size: 16px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`
|
||||||
|
card.appendChild(subtitle)
|
||||||
|
|
||||||
|
// 倒计时文字
|
||||||
|
const timerText = document.createElement('div')
|
||||||
|
timerText.id = 'weekly-timer-text'
|
||||||
|
timerText.textContent = '10'
|
||||||
|
timerText.style.cssText = `
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fcd34d;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
`
|
||||||
|
card.appendChild(timerText)
|
||||||
|
|
||||||
|
// 选项区域
|
||||||
|
const optionsWrap = document.createElement('div')
|
||||||
|
optionsWrap.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
options.forEach((opt, idx) => {
|
||||||
|
const btn = document.createElement('button')
|
||||||
|
btn.textContent = opt
|
||||||
|
btn.dataset.index = String(idx)
|
||||||
|
btn.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 8px;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 2px solid #4b5563;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
`
|
||||||
|
btn.addEventListener('mouseenter', () => {
|
||||||
|
if (!btn.disabled) {
|
||||||
|
btn.style.borderColor = '#7c3aed'
|
||||||
|
btn.style.background = '#1e1b4b'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
btn.addEventListener('mouseleave', () => {
|
||||||
|
if (!btn.disabled) {
|
||||||
|
btn.style.borderColor = '#4b5563'
|
||||||
|
btn.style.background = '#0f172a'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.handleAnswer(idx === correctIndex, options[correctIndex])
|
||||||
|
})
|
||||||
|
optionsWrap.appendChild(btn)
|
||||||
|
})
|
||||||
|
card.appendChild(optionsWrap)
|
||||||
|
|
||||||
|
// 结果区域(初始隐藏)
|
||||||
|
const resultArea = document.createElement('div')
|
||||||
|
resultArea.id = 'weekly-result'
|
||||||
|
resultArea.style.cssText = `
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
`
|
||||||
|
card.appendChild(resultArea)
|
||||||
|
|
||||||
|
// 注入动画样式
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes fadeInOverlay { from { opacity: 0 } to { opacity: 1 } }
|
||||||
|
@keyframes slideInCard { from { transform: translateY(-30px); opacity: 0 } to { transform: translateY(0); opacity: 1 } }
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
|
||||||
|
overlay.appendChild(card)
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
this.container = overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer(): void {
|
||||||
|
let remaining = 10
|
||||||
|
const progressBar = document.getElementById('weekly-progress-bar')
|
||||||
|
const timerText = document.getElementById('weekly-timer-text')
|
||||||
|
|
||||||
|
if (progressBar) {
|
||||||
|
// 使用 CSS transition 驱动进度条(10秒内从100%到0%)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
progressBar.style.transition = 'width 10s linear'
|
||||||
|
progressBar.style.width = '0%'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timerInterval = setInterval(() => {
|
||||||
|
remaining--
|
||||||
|
if (timerText) timerText.textContent = String(remaining)
|
||||||
|
if (remaining <= 0) {
|
||||||
|
this.stopTimer()
|
||||||
|
this.handleAnswer(false, '')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTimer(): void {
|
||||||
|
if (this.timerInterval !== null) {
|
||||||
|
clearInterval(this.timerInterval)
|
||||||
|
this.timerInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAnswer(isCorrect: boolean, correctWord: string): void {
|
||||||
|
this.stopTimer()
|
||||||
|
|
||||||
|
// 禁用所有按钮
|
||||||
|
const buttons = this.container?.querySelectorAll('button')
|
||||||
|
buttons?.forEach(btn => {
|
||||||
|
(btn as HTMLButtonElement).disabled = true
|
||||||
|
;(btn as HTMLButtonElement).style.opacity = '0.5'
|
||||||
|
;(btn as HTMLButtonElement).style.cursor = 'default'
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultArea = document.getElementById('weekly-result')
|
||||||
|
if (!resultArea) return
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
// 随机给 HC 或补满精力
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
const reward = 50 + Math.floor(Math.random() * 51)
|
||||||
|
GameManager.getInstance().addHC(reward)
|
||||||
|
resultArea.textContent = `✓ 正确!"${correctWord}" 就是本周黑话!获得 ${reward} HC!`
|
||||||
|
resultArea.style.background = 'rgba(16,185,129,0.15)'
|
||||||
|
resultArea.style.border = '1px solid #10b981'
|
||||||
|
resultArea.style.color = '#6ee7b7'
|
||||||
|
} else {
|
||||||
|
this.callbacks.onFullStamina()
|
||||||
|
resultArea.textContent = `✓ 正确!"${correctWord}" 完美!全场精力已补满!`
|
||||||
|
resultArea.style.background = 'rgba(16,185,129,0.15)'
|
||||||
|
resultArea.style.border = '1px solid #10b981'
|
||||||
|
resultArea.style.color = '#6ee7b7'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.callbacks.onBossInspection()
|
||||||
|
const msg = correctWord
|
||||||
|
? `✗ 答错了!正确答案是"${correctWord}"!老板来视察了!全场禁锢3秒!`
|
||||||
|
: '✗ 超时了!老板来视察了!全场禁锢3秒!'
|
||||||
|
resultArea.textContent = msg
|
||||||
|
resultArea.style.background = 'rgba(239,68,68,0.15)'
|
||||||
|
resultArea.style.border = '1px solid #ef4444'
|
||||||
|
resultArea.style.color = '#fca5a5'
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArea.style.display = 'block'
|
||||||
|
|
||||||
|
// 2秒后自动关闭
|
||||||
|
setTimeout(() => {
|
||||||
|
this.close()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private close(): void {
|
||||||
|
this.cleanup()
|
||||||
|
this.onClose?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanup(): void {
|
||||||
|
this.stopTimer()
|
||||||
|
const existing = document.getElementById('weekly-report-overlay')
|
||||||
|
if (existing) existing.remove()
|
||||||
|
this.container = null
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user