1.优化后台测试推送功能页面

2.优化开奖和实时对局页面
This commit is contained in:
2026-04-18 17:16:13 +08:00
parent 5c07967bf9
commit c184fa8a46
14 changed files with 582 additions and 78 deletions

View File

@@ -1,12 +1,16 @@
export default {
tip: 'Listen to pushed bet stream in real time and show the AI default number (minimum estimated platform loss).',
tip: 'Realtime bets; after lock the AI default number is fixed for this round and used at countdown end (or your scheduled number). After draw, ~3s payout grace then next round.',
current_record: 'Current round',
ai_default_number: 'AI default number',
pending_draw: 'Scheduled draw number',
countdown: 'Countdown',
bet_countdown: 'Bet left',
draw_countdown: 'Draw left',
payout_countdown: 'Payout left',
payout_na: '—',
payout_phase: 'Payout in progress',
btn_calc: 'Calculate PnL',
btn_draw: 'Draw now',
btn_draw: 'Schedule draw',
calc_result_number: 'Calculated number',
calc_estimated_loss: 'Estimated payout',
push_connected: 'Push connected, realtime updates running',

View File

@@ -1,3 +1,3 @@
export default {
tip: 'Subscribe to public-game-period (global period channel) for period.tick / period.locked / period.opened. The server must publish to this channel.',
tip: 'Subscribe to public-game-period (global period channel) for period.tick / period.locked / period.opened / period.payout. The server must publish to this channel.',
}

View File

@@ -1,12 +1,16 @@
export default {
tip: '实时监听页面推送的压注记录,并展示AI默认最优开奖号码(平台预估亏损最少)',
tip: '实时监听压注记录;封盘后 AI 默认号码会锁定,本期倒计时结束按该号码(或您预约的号码)开奖;开奖后约 3 秒派彩再进入下一期。',
current_record: '当前对局',
ai_default_number: 'AI默认开奖号码',
pending_draw: '已预约开奖号码',
countdown: '倒计时',
bet_countdown: '下注剩余',
draw_countdown: '开奖剩余',
payout_countdown: '派彩剩余',
payout_na: '—',
payout_phase: '派彩中,请稍候',
btn_calc: '计算法盈亏',
btn_draw: '开奖',
btn_draw: '预约开奖',
calc_result_number: '计算开奖号码',
calc_estimated_loss: '计算预估赔付',
push_connected: '推送服务已连接,页面数据实时更新中',

View File

@@ -1,3 +1,3 @@
export default {
tip: '订阅文档中的「全局对局频道」public-game-period用于验证 period.tick / period.locked / period.opened 等公共事件(需服务端向该频道推送)。',
tip: '订阅文档中的「全局对局频道」public-game-period用于验证 period.tick / period.locked / period.opened / period.payout 等公共事件(需服务端向该频道推送)。',
}

View File

@@ -6,7 +6,9 @@ const DOC_EVENTS = [
'period.tick',
'period.locked',
'period.opened',
'period.payout',
'bet.accepted',
'bet.settled',
'wallet.changed',
'notice.popout',
'withdraw.review_required',

View File

@@ -4,10 +4,14 @@
<el-alert :type="pushConnected ? 'success' : 'error'" :title="pushConnected ? t('game.live.push_connected') : t('game.live.push_disconnected')" show-icon class="mb-12" />
<el-card shadow="never" class="mb-12">
<el-alert v-if="snapshot.is_payout_phase" type="warning" :title="t('game.live.payout_phase')" show-icon class="mb-12" />
<div class="header-row">
<div>
<div>{{ t('game.live.current_record') }}: {{ snapshot.record?.period_no || '-' }}</div>
<div>{{ t('game.live.ai_default_number') }}: {{ snapshot.ai_default_number ?? '-' }}</div>
<div v-if="snapshot.pending_draw_number != null">
{{ t('game.live.pending_draw') }}: {{ snapshot.pending_draw_number }}
</div>
<div>{{ t('game.live.countdown') }}: {{ countdownText }}</div>
</div>
<div class="header-actions">
@@ -15,7 +19,7 @@
<el-button :loading="calcLoading" :disabled="!snapshot.can_calculate" @click="onCalculate">
{{ t('game.live.btn_calc') }}
</el-button>
<el-button type="primary" :loading="drawLoading" :disabled="!snapshot.can_draw" @click="onDraw">
<el-button type="primary" :loading="drawLoading" :disabled="!snapshot.can_schedule_draw" @click="onDraw">
{{ t('game.live.btn_draw') }}
</el-button>
<el-button :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
@@ -67,6 +71,7 @@ interface Snapshot {
bets: anyObj[]
candidate_numbers: anyObj[]
ai_default_number: number | null
pending_draw_number: number | null
period_seconds?: number
bet_seconds?: number
pick_max_number_count?: number
@@ -74,8 +79,12 @@ interface Snapshot {
draw_number_max?: number
remaining_seconds?: number
bet_remaining_seconds?: number
payout_remaining_seconds?: number
is_payout_phase?: boolean
can_calculate?: boolean
can_draw?: boolean
can_schedule_draw?: boolean
server_time?: number
}
const { t } = useI18n()
@@ -87,14 +96,18 @@ const snapshot = reactive<Snapshot>({
bets: [],
candidate_numbers: [],
ai_default_number: null,
pending_draw_number: null,
period_seconds: 30,
bet_seconds: 20,
pick_max_number_count: 10,
draw_number_max: 36,
remaining_seconds: 0,
bet_remaining_seconds: 0,
payout_remaining_seconds: 0,
is_payout_phase: false,
can_calculate: false,
can_draw: false,
can_schedule_draw: false,
})
const calcLoading = ref(false)
const drawLoading = ref(false)
@@ -102,6 +115,12 @@ const manualNumber = ref<number | null>(1)
const calcResultNumber = ref<number | null>(null)
const calcEstimatedLoss = ref<string>('0.0000')
/** 服务端 Unix 秒 本地 Unix 秒,用于派彩倒计时与服务器对齐 */
const serverSkewSeconds = ref(0)
/** 每秒递增,驱动派彩剩余秒本地刷新 */
const clockTick = ref(0)
let clockTimer: number | null = null
let pushClient: any = null
let pushChannel: any = null
let pollTimer: number | null = null
@@ -113,6 +132,45 @@ function formatPicks(v: unknown): string {
return '-'
}
function syncServerClock(serverTime: unknown): void {
if (typeof serverTime === 'number' && Number.isFinite(serverTime)) {
serverSkewSeconds.value = serverTime - Math.floor(Date.now() / 1000)
snapshot.server_time = serverTime
}
}
function readPayoutUntilUnix(rec: anyObj | null): number | null {
if (!rec) {
return null
}
const v = rec.payout_until
if (v === null || v === undefined || v === '') {
return null
}
if (typeof v === 'number' && Number.isFinite(v)) {
return v
}
if (typeof v === 'string' && /^\d+$/.test(v)) {
return parseInt(v, 10)
}
return null
}
/** 派彩剩余秒:优先用 payout_until 与对时后的「服务器当前秒」计算,便于每秒递减 */
const payoutRemainingLive = computed(() => {
clockTick.value
if (!snapshot.is_payout_phase) {
return null
}
const until = readPayoutUntilUnix(snapshot.record)
if (until !== null) {
const serverNow = Math.floor(Date.now() / 1000) + serverSkewSeconds.value
const diff = until - serverNow
return diff > 0 ? diff : 0
}
return snapshot.payout_remaining_seconds ?? 0
})
async function loadSnapshot() {
loading.value = true
try {
@@ -122,14 +180,20 @@ async function loadSnapshot() {
snapshot.bets = res.data.bets || []
snapshot.candidate_numbers = res.data.candidate_numbers || []
snapshot.ai_default_number = res.data.ai_default_number
snapshot.pending_draw_number =
typeof res.data.pending_draw_number === 'number' ? res.data.pending_draw_number : null
snapshot.period_seconds = res.data.period_seconds ?? 30
snapshot.bet_seconds = res.data.bet_seconds ?? 20
snapshot.pick_max_number_count = res.data.pick_max_number_count ?? 10
snapshot.draw_number_max = res.data.draw_number_max ?? 36
snapshot.remaining_seconds = res.data.remaining_seconds ?? 0
snapshot.bet_remaining_seconds = res.data.bet_remaining_seconds ?? 0
snapshot.payout_remaining_seconds = res.data.payout_remaining_seconds ?? 0
snapshot.is_payout_phase = !!res.data.is_payout_phase
snapshot.can_calculate = !!res.data.can_calculate
snapshot.can_draw = !!res.data.can_draw
snapshot.can_schedule_draw = !!res.data.can_schedule_draw || !!res.data.can_draw
syncServerClock(res.data.server_time)
const dmax = res.data.draw_number_max ?? 36
if (manualNumber.value === null || manualNumber.value < 1 || manualNumber.value > dmax) manualNumber.value = 1
}
@@ -172,14 +236,20 @@ async function initPush() {
snapshot.bets = payload.bets || []
snapshot.candidate_numbers = payload.candidate_numbers || []
snapshot.ai_default_number = payload.ai_default_number ?? null
snapshot.pending_draw_number =
typeof payload.pending_draw_number === 'number' ? payload.pending_draw_number : null
snapshot.period_seconds = payload.period_seconds ?? 30
snapshot.bet_seconds = payload.bet_seconds ?? 20
snapshot.pick_max_number_count = payload.pick_max_number_count ?? 10
snapshot.draw_number_max = payload.draw_number_max ?? 36
snapshot.remaining_seconds = payload.remaining_seconds ?? 0
snapshot.bet_remaining_seconds = payload.bet_remaining_seconds ?? 0
snapshot.payout_remaining_seconds = payload.payout_remaining_seconds ?? 0
snapshot.is_payout_phase = !!payload.is_payout_phase
snapshot.can_calculate = !!payload.can_calculate
snapshot.can_draw = !!payload.can_draw
snapshot.can_schedule_draw = !!payload.can_schedule_draw || !!payload.can_draw
syncServerClock(payload.server_time)
})
} catch {
pushConnected.value = false
@@ -244,12 +314,19 @@ async function onDraw() {
}
const countdownText = computed(() => {
const total = snapshot.remaining_seconds ?? 0
const bet = snapshot.bet_remaining_seconds ?? 0
return `${t('game.live.bet_countdown')} ${bet}s / ${t('game.live.draw_countdown')} ${total}s`
const draw = snapshot.remaining_seconds ?? 0
let payoutPart = t('game.live.payout_na')
if (snapshot.is_payout_phase && payoutRemainingLive.value !== null) {
payoutPart = `${payoutRemainingLive.value}s`
}
return `${t('game.live.bet_countdown')} ${bet}s / ${t('game.live.draw_countdown')} ${draw}s / ${t('game.live.payout_countdown')} ${payoutPart}`
})
onMounted(async () => {
clockTimer = window.setInterval(() => {
clockTick.value++
}, 1000)
await loadSnapshot()
try {
await initPush()
@@ -269,6 +346,10 @@ onUnmounted(() => {
}
stopPolling()
stopPushWatchdog()
if (clockTimer !== null) {
window.clearInterval(clockTimer)
clockTimer = null
}
})
function startPolling() {

View File

@@ -63,7 +63,7 @@ const logText = computed(() => {
})
function appendLog(line: PushTestLogLine) {
logs.value = [...logs.value, line].slice(-200)
logs.value = [line, ...logs.value].slice(0, 200)
}
function clearLogs() {