优化实时游戏对局页面

This commit is contained in:
2026-04-20 10:49:36 +08:00
parent cf9373e5c2
commit b019fc2a62
3 changed files with 281 additions and 36 deletions

View File

@@ -9,6 +9,8 @@ export default {
payout_countdown: 'Payout left',
payout_na: '—',
payout_phase: 'Payout in progress',
action_panel: 'Actions',
manual_draw_number: 'Scheduled draw',
btn_calc: 'Calculate PnL',
btn_draw: 'Schedule draw',
calc_result_number: 'Calculated number',

View File

@@ -9,7 +9,9 @@ export default {
payout_countdown: '派彩剩余',
payout_na: '—',
payout_phase: '派彩中,请稍候',
btn_calc: '计算法盈亏',
action_panel: '操作区',
manual_draw_number: '预约开奖',
btn_calc: '计算盈亏',
btn_draw: '预约开奖',
calc_result_number: '计算开奖号码',
calc_estimated_loss: '计算预估赔付',

View File

@@ -3,31 +3,75 @@
<el-alert type="info" :title="t('game.live.tip')" show-icon class="mb-12" />
<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-card shadow="never" class="mb-12 live-control-card">
<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 class="live-control-layout">
<div class="live-control-main">
<el-descriptions :column="1" border size="small" class="live-desc">
<el-descriptions-item :label="t('game.live.current_record')">
<span class="period-no">{{ snapshot.record?.period_no || '—' }}</span>
</el-descriptions-item>
<el-descriptions-item :label="t('game.live.ai_default_number')">
<span class="num-em">{{ snapshot.ai_default_number ?? '—' }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="snapshot.pending_draw_number != null" :label="t('game.live.pending_draw')">
<el-tag type="primary" effect="plain">{{ snapshot.pending_draw_number }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="countdown-block">
<div class="countdown-block__title">{{ t('game.live.countdown') }}</div>
<div class="countdown-cards">
<div class="cd-card">
<span class="cd-card__label">{{ t('game.live.bet_countdown') }}</span>
<span class="cd-card__val">{{ countdownParts.bet }}s</span>
</div>
<div class="cd-card">
<span class="cd-card__label">{{ t('game.live.draw_countdown') }}</span>
<span class="cd-card__val">{{ countdownParts.draw }}s</span>
</div>
<div class="cd-card" :class="{ 'is-active': snapshot.is_payout_phase }">
<span class="cd-card__label">{{ t('game.live.payout_countdown') }}</span>
<span class="cd-card__val">{{ countdownParts.payout }}</span>
</div>
</div>
</div>
<div class="calc-result-bar">
<span class="calc-result-bar__item">
<span class="calc-result-bar__k">{{ t('game.live.calc_result_number') }}</span>
<span class="calc-result-bar__v">{{ calcResultNumber ?? '—' }}</span>
</span>
<span class="calc-result-bar__item">
<span class="calc-result-bar__k">{{ t('game.live.calc_estimated_loss') }}</span>
<span class="calc-result-bar__v mono">{{ calcEstimatedLoss }}</span>
</span>
</div>
<div>{{ t('game.live.countdown') }}: {{ countdownText }}</div>
</div>
<div class="header-actions">
<el-input-number v-model="manualNumber" :min="1" :max="snapshot.draw_number_max ?? 36" :step="1" />
<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_schedule_draw" @click="onDraw">
{{ t('game.live.btn_draw') }}
</el-button>
<el-button :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
</div>
</div>
<div class="result-row">
<span>{{ t('game.live.calc_result_number') }}: {{ calcResultNumber ?? '-' }}</span>
<span>{{ t('game.live.calc_estimated_loss') }}: {{ calcEstimatedLoss }}</span>
<aside class="live-control-aside">
<div class="aside-title">{{ t('game.live.action_panel') }}</div>
<div class="aside-field">
<span class="aside-field__label">{{ t('game.live.manual_draw_number') }}</span>
<el-input-number
v-model="manualNumber"
class="aside-field__input"
:min="1"
:max="snapshot.draw_number_max ?? 36"
:step="1"
controls-position="right"
/>
</div>
<div class="aside-btns">
<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_schedule_draw" @click="onDraw">
{{ t('game.live.btn_draw') }}
</el-button>
<el-button :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
</div>
</aside>
</div>
</el-card>
@@ -313,14 +357,14 @@ async function onDraw() {
}
}
const countdownText = computed(() => {
const countdownParts = computed(() => {
const bet = snapshot.bet_remaining_seconds ?? 0
const draw = snapshot.remaining_seconds ?? 0
let payoutPart = t('game.live.payout_na')
let payout = t('game.live.payout_na')
if (snapshot.is_payout_phase && payoutRemainingLive.value !== null) {
payoutPart = `${payoutRemainingLive.value}s`
payout = `${payoutRemainingLive.value}s`
}
return `${t('game.live.bet_countdown')} ${bet}s / ${t('game.live.draw_countdown')} ${draw}s / ${t('game.live.payout_countdown')} ${payoutPart}`
return { bet, draw, payout }
})
onMounted(async () => {
@@ -394,20 +438,217 @@ function stopPushWatchdog() {
.mb-12 {
margin-bottom: 12px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.live-control-card {
:deep(.el-card__body) {
padding-top: 8px;
}
}
.header-actions {
.live-control-layout {
display: flex;
flex-wrap: wrap;
gap: 20px 24px;
align-items: flex-start;
}
.live-control-main {
flex: 1 1 320px;
min-width: 0;
}
.live-desc {
margin-bottom: 16px;
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
}
.period-no {
display: inline-block;
max-width: 100%;
word-break: break-all;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
line-height: 1.5;
color: var(--el-text-color-primary);
}
.num-em {
font-size: 16px;
font-weight: 600;
color: var(--el-color-primary);
}
.countdown-block {
margin-bottom: 14px;
}
.countdown-block__title {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.countdown-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.cd-card {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 10px 12px;
background: var(--el-fill-color-blank);
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
text-align: center;
min-width: 0;
}
/* 移动端保持同一排,压缩间距与字号以省纵向空间 */
@media (max-width: 768px) {
.countdown-block {
margin-bottom: 10px;
}
.countdown-block__title {
margin-bottom: 6px;
}
.countdown-cards {
gap: 6px;
}
.cd-card {
padding: 6px 4px;
border-radius: 6px;
gap: 2px;
min-width: 0;
}
.cd-card__label {
font-size: 11px;
line-height: 1.2;
padding: 0 2px;
}
.cd-card__val {
font-size: 15px;
}
}
.cd-card.is-active {
border-color: var(--el-color-warning-light-5);
background: var(--el-color-warning-light-9);
}
.cd-card__label {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.3;
}
.cd-card__val {
font-size: 18px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--el-text-color-primary);
}
.calc-result-bar {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
padding: 10px 14px;
border-radius: 8px;
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
}
.calc-result-bar__item {
display: inline-flex;
align-items: baseline;
gap: 8px;
}
.result-row {
margin-top: 12px;
.calc-result-bar__k {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.calc-result-bar__v {
font-size: 14px;
font-weight: 500;
}
.calc-result-bar__v.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.live-control-aside {
flex: 0 0 240px;
padding-left: 20px;
border-left: 1px solid var(--el-border-color-lighter);
display: flex;
gap: 16px;
flex-direction: column;
gap: 12px;
}
@media (max-width: 992px) {
.live-control-aside {
flex: 1 1 100%;
padding-left: 0;
border-left: none;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
}
.aside-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-regular);
}
.aside-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.aside-field__label {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.aside-field__input {
width: 100%;
}
.aside-btns {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 8px;
align-items: stretch;
}
.aside-btns .el-button {
flex: 1;
min-width: 0;
margin-left: 0;
padding-left: 8px;
padding-right: 8px;
}
.aside-btns .el-button :deep(span) {
white-space: normal;
line-height: 1.25;
text-align: center;
}
</style>