优化游戏实时对局页面
This commit is contained in:
@@ -26,4 +26,17 @@ export default {
|
||||
pick_numbers: 'Pick numbers',
|
||||
total_amount: 'Total bet amount',
|
||||
streak_at_bet: 'Streak at bet',
|
||||
runtime_switch: 'Game runtime',
|
||||
countdown_maintenance: 'Maintenance',
|
||||
runtime_draining_banner:
|
||||
'Game stopped: the current round will run through draw, settlement and payout. Full maintenance UI appears after payout completes.',
|
||||
runtime_maintenance_banner:
|
||||
'Maintenance: player betting is disabled. Turn runtime on to resume; a new round is created when idle.',
|
||||
runtime_off_tip: 'When turning runtime on with no active round, a new period is created immediately.',
|
||||
void_btn: 'Void round',
|
||||
void_dialog_title: 'Void current round',
|
||||
void_reason_label: 'Reason',
|
||||
void_reason_placeholder: 'Enter the reason (stored on the record; pending bets will be refunded).',
|
||||
void_submit: 'Confirm void',
|
||||
void_reason_too_short: 'Reason must be at least 2 characters',
|
||||
}
|
||||
|
||||
@@ -26,4 +26,16 @@ export default {
|
||||
pick_numbers: '压注号码',
|
||||
total_amount: '压注总额',
|
||||
streak_at_bet: '下注时连胜',
|
||||
runtime_switch: '游戏运行',
|
||||
countdown_maintenance: '维护中',
|
||||
runtime_draining_banner:
|
||||
'已关闭游戏:当前局将正常进行至开奖、结算并完成派彩;全部结束后进入维护模式(倒计时与操作区将切换为维护中)。',
|
||||
runtime_maintenance_banner: '维护中:玩家端已禁止下注。请开启「游戏运行」恢复;若无进行中的局将自动创建新一期。',
|
||||
runtime_off_tip: '开启「游戏运行」后,若无进行中的局将立即创建新一期。',
|
||||
void_btn: '作废本局',
|
||||
void_dialog_title: '作废本局',
|
||||
void_reason_label: '作废原因',
|
||||
void_reason_placeholder: '请填写本期作废原因(将写入对局记录并退款待开奖注单)',
|
||||
void_submit: '确认作废',
|
||||
void_reason_too_short: '作废原因至少 2 个字符',
|
||||
}
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
<div class="default-main">
|
||||
<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-alert
|
||||
v-if="snapshot.runtime_enabled === false && !snapshot.maintenance_ui"
|
||||
type="warning"
|
||||
:title="t('game.live.runtime_draining_banner')"
|
||||
show-icon
|
||||
class="mb-12"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="snapshot.maintenance_ui"
|
||||
type="warning"
|
||||
:title="t('game.live.runtime_maintenance_banner')"
|
||||
show-icon
|
||||
class="mb-12"
|
||||
/>
|
||||
|
||||
<div class="live-top-toolbar">
|
||||
<div class="live-top-toolbar__row">
|
||||
<span class="live-top-toolbar__label">{{ t('game.live.runtime_switch') }}</span>
|
||||
<el-switch
|
||||
:model-value="snapshot.runtime_enabled"
|
||||
:loading="runtimeSwitchLoading"
|
||||
:disabled="runtimeSwitchLoading || voidSubmitting"
|
||||
@change="onRuntimeSwitch"
|
||||
/>
|
||||
<span v-if="snapshot.maintenance_ui" class="live-top-toolbar__hint">{{ t('game.live.runtime_off_tip') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
@@ -21,7 +48,10 @@
|
||||
|
||||
<div class="countdown-block">
|
||||
<div class="countdown-block__title">{{ t('game.live.countdown') }}</div>
|
||||
<div class="countdown-cards">
|
||||
<div v-if="snapshot.maintenance_ui" class="countdown-maintenance">
|
||||
{{ t('game.live.countdown_maintenance') }}
|
||||
</div>
|
||||
<div v-else 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>
|
||||
@@ -49,8 +79,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="live-control-aside">
|
||||
<aside class="live-control-aside" :class="{ 'is-locked': asideOperationLocked }">
|
||||
<div class="aside-title">{{ t('game.live.action_panel') }}</div>
|
||||
<el-button
|
||||
class="aside-void-btn"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="asideOperationLocked || !canVoidPeriod || voidSubmitting || runtimeSwitchLoading"
|
||||
@click="openVoidDialog"
|
||||
>
|
||||
{{ t('game.live.void_btn') }}
|
||||
</el-button>
|
||||
<div class="aside-field">
|
||||
<span class="aside-field__label">{{ t('game.live.manual_draw_number') }}</span>
|
||||
<el-input-number
|
||||
@@ -59,17 +98,27 @@
|
||||
:min="1"
|
||||
:max="snapshot.draw_number_max ?? 36"
|
||||
:step="1"
|
||||
:disabled="asideOperationLocked"
|
||||
controls-position="right"
|
||||
/>
|
||||
</div>
|
||||
<div class="aside-btns">
|
||||
<el-button :loading="calcLoading" :disabled="!snapshot.can_calculate" @click="onCalculate">
|
||||
<el-button
|
||||
:loading="calcLoading"
|
||||
:disabled="asideOperationLocked || !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">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="drawLoading"
|
||||
:disabled="asideOperationLocked || !snapshot.can_schedule_draw"
|
||||
@click="onDraw"
|
||||
>
|
||||
{{ t('game.live.btn_draw') }}
|
||||
</el-button>
|
||||
<el-button :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
|
||||
<el-button :loading="loading" :disabled="asideOperationLocked" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -102,12 +151,45 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog
|
||||
v-model="voidDialogVisible"
|
||||
class="live-void-dialog"
|
||||
:title="t('game.live.void_dialog_title')"
|
||||
width="520px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@closed="voidReason = ''"
|
||||
>
|
||||
<el-form class="live-void-form" label-position="top" @submit.prevent>
|
||||
<el-form-item :label="t('game.live.void_reason_label')">
|
||||
<el-input
|
||||
v-model="voidReason"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||
:placeholder="t('game.live.void_reason_placeholder')"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="live-void-footer">
|
||||
<el-button @click="voidDialogVisible = false">{{ t('Cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="voidSubmitting" @click="submitVoidPeriod">{{ t('game.live.void_submit') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import createAxios, { getPushScriptUrl } from '/@/utils/axios'
|
||||
|
||||
interface Snapshot {
|
||||
@@ -129,6 +211,10 @@ interface Snapshot {
|
||||
can_draw?: boolean
|
||||
can_schedule_draw?: boolean
|
||||
server_time?: number
|
||||
/** 游戏运行开关:false 表示已关服(当局仍可进行至派彩结束) */
|
||||
runtime_enabled?: boolean
|
||||
/** 完整维护 UI:关服且当前无进行中/未结清对局(派彩已全部完成) */
|
||||
maintenance_ui?: boolean
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -151,9 +237,15 @@ const snapshot = reactive<Snapshot>({
|
||||
can_calculate: false,
|
||||
can_draw: false,
|
||||
can_schedule_draw: false,
|
||||
runtime_enabled: true,
|
||||
maintenance_ui: false,
|
||||
})
|
||||
const calcLoading = ref(false)
|
||||
const drawLoading = ref(false)
|
||||
const runtimeSwitchLoading = ref(false)
|
||||
const voidDialogVisible = ref(false)
|
||||
const voidReason = ref('')
|
||||
const voidSubmitting = ref(false)
|
||||
const manualNumber = ref<number | null>(1)
|
||||
const calcResultNumber = ref<number | null>(null)
|
||||
const calcEstimatedLoss = ref<string>('0.0000')
|
||||
@@ -175,6 +267,47 @@ function formatPicks(v: unknown): string {
|
||||
return '-'
|
||||
}
|
||||
|
||||
const canVoidPeriod = computed(() => {
|
||||
const r = snapshot.record
|
||||
if (!r) {
|
||||
return false
|
||||
}
|
||||
const s = Number(r.status)
|
||||
return s === 0 || s === 1
|
||||
})
|
||||
|
||||
/** 派彩结束后的完整维护态:操作区除顶部开关外全部锁定 */
|
||||
const asideOperationLocked = computed(() => snapshot.maintenance_ui === true)
|
||||
|
||||
function mergeLiveSnapshot(data: anyObj): void {
|
||||
if (data.record !== undefined) {
|
||||
snapshot.record = data.record
|
||||
}
|
||||
snapshot.bets = data.bets || []
|
||||
snapshot.candidate_numbers = data.candidate_numbers || []
|
||||
snapshot.ai_default_number = data.ai_default_number ?? null
|
||||
snapshot.pending_draw_number = typeof data.pending_draw_number === 'number' ? data.pending_draw_number : null
|
||||
snapshot.period_seconds = data.period_seconds ?? 30
|
||||
snapshot.bet_seconds = data.bet_seconds ?? 20
|
||||
snapshot.pick_max_number_count = data.pick_max_number_count ?? 10
|
||||
snapshot.draw_number_max = data.draw_number_max ?? 36
|
||||
snapshot.remaining_seconds = data.remaining_seconds ?? 0
|
||||
snapshot.bet_remaining_seconds = data.bet_remaining_seconds ?? 0
|
||||
snapshot.payout_remaining_seconds = data.payout_remaining_seconds ?? 0
|
||||
snapshot.is_payout_phase = !!data.is_payout_phase
|
||||
snapshot.can_calculate = !!data.can_calculate
|
||||
snapshot.can_draw = !!data.can_draw
|
||||
snapshot.can_schedule_draw = !!(data.can_schedule_draw || data.can_draw)
|
||||
if (typeof data.runtime_enabled === 'boolean') {
|
||||
snapshot.runtime_enabled = data.runtime_enabled
|
||||
}
|
||||
if (typeof data.maintenance_ui === 'boolean') {
|
||||
snapshot.maintenance_ui = data.maintenance_ui
|
||||
}
|
||||
syncServerClock(data.server_time)
|
||||
}
|
||||
|
||||
|
||||
function syncServerClock(serverTime: unknown): void {
|
||||
if (typeof serverTime === 'number' && Number.isFinite(serverTime)) {
|
||||
serverSkewSeconds.value = serverTime - Math.floor(Date.now() / 1000)
|
||||
@@ -219,24 +352,7 @@ async function loadSnapshot() {
|
||||
try {
|
||||
const res = await createAxios({ url: '/admin/game.Live/snapshot', method: 'get', showCodeMessage: false })
|
||||
if (res.code === 1 && res.data) {
|
||||
snapshot.record = res.data.record
|
||||
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)
|
||||
mergeLiveSnapshot(res.data as anyObj)
|
||||
const dmax = res.data.draw_number_max ?? 36
|
||||
if (manualNumber.value === null || manualNumber.value < 1 || manualNumber.value > dmax) manualNumber.value = 1
|
||||
}
|
||||
@@ -245,6 +361,62 @@ async function loadSnapshot() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onRuntimeSwitch(val: boolean | string | number): void {
|
||||
const on = val === true || val === 'true' || val === 1
|
||||
runtimeSwitchLoading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/game.Live/runtime',
|
||||
method: 'post',
|
||||
data: { enabled: on ? 1 : 0 },
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
mergeLiveSnapshot(res.data as anyObj)
|
||||
} else {
|
||||
await loadSnapshot()
|
||||
}
|
||||
} catch {
|
||||
await loadSnapshot()
|
||||
} finally {
|
||||
runtimeSwitchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openVoidDialog(): void {
|
||||
voidReason.value = ''
|
||||
voidDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitVoidPeriod(): Promise<void> {
|
||||
const reason = voidReason.value.trim()
|
||||
if (reason.length < 2) {
|
||||
ElMessage.warning(t('game.live.void_reason_too_short'))
|
||||
return
|
||||
}
|
||||
if (!snapshot.record) {
|
||||
return
|
||||
}
|
||||
voidSubmitting.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/game.Live/voidPeriod',
|
||||
method: 'post',
|
||||
data: {
|
||||
record_id: snapshot.record.id,
|
||||
void_reason: reason,
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
mergeLiveSnapshot(res.data as anyObj)
|
||||
}
|
||||
voidDialogVisible.value = false
|
||||
} finally {
|
||||
voidSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initPush() {
|
||||
const cfgRes = await createAxios({ url: '/admin/game.Live/pushConfig', method: 'get', showCodeMessage: false })
|
||||
if (cfgRes.code !== 1 || !cfgRes.data) {
|
||||
@@ -277,24 +449,7 @@ async function initPush() {
|
||||
stopPolling()
|
||||
pushChannel.on(event, (payload: anyObj) => {
|
||||
pushConnected.value = true
|
||||
snapshot.record = payload.record || null
|
||||
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)
|
||||
mergeLiveSnapshot(payload)
|
||||
})
|
||||
} catch {
|
||||
pushConnected.value = false
|
||||
@@ -444,6 +599,34 @@ function stopPushWatchdog() {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.live-top-toolbar {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-bg-color-overlay);
|
||||
}
|
||||
|
||||
.live-top-toolbar__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px 20px;
|
||||
}
|
||||
|
||||
.live-top-toolbar__label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.live-top-toolbar__hint {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
flex: 1 1 220px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.live-control-card {
|
||||
:deep(.el-card__body) {
|
||||
padding-top: 8px;
|
||||
@@ -496,6 +679,17 @@ function stopPushWatchdog() {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.countdown-maintenance {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-warning-dark-2);
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.countdown-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -602,6 +796,11 @@ function stopPushWatchdog() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&.is-locked {
|
||||
opacity: 0.9;
|
||||
filter: grayscale(0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@@ -620,6 +819,11 @@ function stopPushWatchdog() {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.aside-void-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aside-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -657,3 +861,67 @@ function stopPushWatchdog() {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* Dialog teleport 到 body,独立块以便样式命中 */
|
||||
.live-void-dialog.el-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(92vh, 720px);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.live-void-dialog .el-dialog__header {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 16px 10px;
|
||||
}
|
||||
|
||||
.live-void-dialog .el-dialog__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 12px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.live-void-dialog .el-dialog__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.live-void-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.live-void-form .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.live-void-dialog.el-dialog {
|
||||
width: calc(100vw - 24px) !important;
|
||||
max-width: 520px;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.live-void-dialog .el-dialog__title {
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.live-void-footer {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.live-void-footer .el-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user