后台游戏对局实时显示-优化
This commit is contained in:
@@ -1,14 +1,29 @@
|
||||
<template>
|
||||
<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-card shadow="never" 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>{{ t('game.live.countdown') }}: {{ countdownText }}</div>
|
||||
</div>
|
||||
<el-button type="primary" :loading="loading" @click="loadSnapshot">{{ t('Refresh') }}</el-button>
|
||||
<div class="header-actions">
|
||||
<el-input-number v-model="manualNumber" :min="1" :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_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>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -43,7 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
@@ -52,19 +67,42 @@ interface Snapshot {
|
||||
bets: anyObj[]
|
||||
candidate_numbers: anyObj[]
|
||||
ai_default_number: number | null
|
||||
period_seconds?: number
|
||||
bet_seconds?: number
|
||||
pick_max_number_count?: number
|
||||
remaining_seconds?: number
|
||||
bet_remaining_seconds?: number
|
||||
can_calculate?: boolean
|
||||
can_draw?: boolean
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const pushConnected = ref(false)
|
||||
const lastPushAt = ref(0)
|
||||
const snapshot = reactive<Snapshot>({
|
||||
record: null,
|
||||
bets: [],
|
||||
candidate_numbers: [],
|
||||
ai_default_number: null,
|
||||
period_seconds: 30,
|
||||
bet_seconds: 20,
|
||||
pick_max_number_count: 36,
|
||||
remaining_seconds: 0,
|
||||
bet_remaining_seconds: 0,
|
||||
can_calculate: false,
|
||||
can_draw: false,
|
||||
})
|
||||
const calcLoading = ref(false)
|
||||
const drawLoading = ref(false)
|
||||
const manualNumber = ref<number | null>(1)
|
||||
const calcResultNumber = ref<number | null>(null)
|
||||
const calcEstimatedLoss = ref<string>('0.0000')
|
||||
|
||||
let pushClient: any = null
|
||||
let pushChannel: any = null
|
||||
let pollTimer: number | null = null
|
||||
let pushWatchdogTimer: number | null = null
|
||||
|
||||
function formatPicks(v: unknown): string {
|
||||
if (Array.isArray(v)) return JSON.stringify(v)
|
||||
@@ -81,6 +119,14 @@ 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.period_seconds = res.data.period_seconds ?? 30
|
||||
snapshot.bet_seconds = res.data.bet_seconds ?? 20
|
||||
snapshot.pick_max_number_count = 36
|
||||
snapshot.remaining_seconds = res.data.remaining_seconds ?? 0
|
||||
snapshot.bet_remaining_seconds = res.data.bet_remaining_seconds ?? 0
|
||||
snapshot.can_calculate = !!res.data.can_calculate
|
||||
snapshot.can_draw = !!res.data.can_draw
|
||||
if (manualNumber.value === null || manualNumber.value < 1 || manualNumber.value > 36) manualNumber.value = 1
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -90,23 +136,49 @@ async function loadSnapshot() {
|
||||
async function initPush() {
|
||||
const cfgRes = await createAxios({ url: '/admin/game.Live/pushConfig', method: 'get', showCodeMessage: false })
|
||||
if (cfgRes.code !== 1 || !cfgRes.data) {
|
||||
pushConnected.value = false
|
||||
return
|
||||
}
|
||||
const { url, app_key, channel, event } = cfgRes.data
|
||||
|
||||
await loadPushJs()
|
||||
const PushCtor = (window as any).Push
|
||||
if (!PushCtor) {
|
||||
try {
|
||||
await loadPushJs()
|
||||
} catch {
|
||||
pushConnected.value = false
|
||||
startPolling()
|
||||
return
|
||||
}
|
||||
pushClient = new PushCtor({ url, app_key })
|
||||
pushChannel = pushClient.subscribe(channel)
|
||||
pushChannel.on(event, (payload: anyObj) => {
|
||||
snapshot.record = payload.record || null
|
||||
snapshot.bets = payload.bets || []
|
||||
snapshot.candidate_numbers = payload.candidate_numbers || []
|
||||
snapshot.ai_default_number = payload.ai_default_number ?? null
|
||||
})
|
||||
const PushCtor = (window as any).Push
|
||||
if (!PushCtor) {
|
||||
pushConnected.value = false
|
||||
startPolling()
|
||||
return
|
||||
}
|
||||
try {
|
||||
pushClient = new PushCtor({ url, app_key })
|
||||
pushChannel = pushClient.subscribe(channel)
|
||||
pushConnected.value = false
|
||||
startPushWatchdog()
|
||||
stopPolling()
|
||||
pushChannel.on(event, (payload: anyObj) => {
|
||||
lastPushAt.value = Date.now()
|
||||
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.period_seconds = payload.period_seconds ?? 30
|
||||
snapshot.bet_seconds = payload.bet_seconds ?? 20
|
||||
snapshot.pick_max_number_count = 36
|
||||
snapshot.remaining_seconds = payload.remaining_seconds ?? 0
|
||||
snapshot.bet_remaining_seconds = payload.bet_remaining_seconds ?? 0
|
||||
snapshot.can_calculate = !!payload.can_calculate
|
||||
snapshot.can_draw = !!payload.can_draw
|
||||
})
|
||||
} catch {
|
||||
pushConnected.value = false
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPushJs() {
|
||||
@@ -122,10 +194,113 @@ async function loadPushJs() {
|
||||
})
|
||||
}
|
||||
|
||||
async function onCalculate() {
|
||||
if (!snapshot.record) return
|
||||
calcLoading.value = true
|
||||
try {
|
||||
const res = await createAxios({
|
||||
url: '/admin/game.Live/calculate',
|
||||
method: 'post',
|
||||
data: {
|
||||
record_id: snapshot.record.id,
|
||||
manual_number: manualNumber.value,
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
if (res.code === 1 && res.data) {
|
||||
snapshot.candidate_numbers = res.data.candidate_numbers || []
|
||||
snapshot.ai_default_number = res.data.ai_default_number ?? null
|
||||
calcResultNumber.value = res.data.final_number ?? null
|
||||
calcEstimatedLoss.value = String(res.data.final_estimated_loss ?? '0.0000')
|
||||
}
|
||||
} finally {
|
||||
calcLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onDraw() {
|
||||
if (!snapshot.record) return
|
||||
drawLoading.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/game.Live/draw',
|
||||
method: 'post',
|
||||
data: {
|
||||
record_id: snapshot.record.id,
|
||||
manual_number: manualNumber.value,
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
await loadSnapshot()
|
||||
} finally {
|
||||
drawLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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`
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSnapshot()
|
||||
await initPush()
|
||||
try {
|
||||
await initPush()
|
||||
} catch {
|
||||
pushConnected.value = false
|
||||
startPolling()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
try {
|
||||
if (pushClient && typeof pushClient.disconnect === 'function') {
|
||||
pushClient.disconnect()
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
stopPolling()
|
||||
stopPushWatchdog()
|
||||
})
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer !== null) {
|
||||
return
|
||||
}
|
||||
pollTimer = window.setInterval(() => {
|
||||
void loadSnapshot()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer !== null) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startPushWatchdog() {
|
||||
if (pushWatchdogTimer !== null) {
|
||||
return
|
||||
}
|
||||
pushWatchdogTimer = window.setInterval(() => {
|
||||
const state = pushClient && pushClient.connection ? String(pushClient.connection.state || '') : ''
|
||||
const stateConnected = state === 'connected' || state === 'connecting'
|
||||
const hasRecentPush = lastPushAt.value > 0 && Date.now() - lastPushAt.value <= 6000
|
||||
if (!stateConnected || !hasRecentPush) {
|
||||
pushConnected.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stopPushWatchdog() {
|
||||
if (pushWatchdogTimer !== null) {
|
||||
window.clearInterval(pushWatchdogTimer)
|
||||
pushWatchdogTimer = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -136,5 +311,16 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.result-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,31 +2,8 @@
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<el-card v-if="canSettings" class="record-settings-card" shadow="never">
|
||||
<template #header>
|
||||
<span>{{ t('game.record.section_auto') }}</span>
|
||||
</template>
|
||||
<div v-loading="settingsLoading" class="record-settings-body">
|
||||
<div class="record-setting-row">
|
||||
<span class="record-setting-label">{{ t('game.record.auto_create_label') }}</span>
|
||||
<el-switch v-model="autoCreate" :disabled="settingsSaving" @change="onSwitchChange" />
|
||||
<span class="record-setting-tip">{{ t('game.record.auto_create_tip') }}</span>
|
||||
</div>
|
||||
<div class="record-setting-row">
|
||||
<span class="record-setting-label">{{ t('game.record.manual_create_label') }}</span>
|
||||
<el-switch v-model="manualCreate" :disabled="settingsSaving" @change="onSwitchChange" />
|
||||
<span class="record-setting-tip">{{ t('game.record.manual_create_tip') }}</span>
|
||||
</div>
|
||||
<div v-if="canManual" class="record-setting-actions">
|
||||
<el-button type="primary" :loading="createLoading" @click="onCreateNextManual">
|
||||
{{ t('game.record.btn_create_next') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:buttons="['refresh', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('game.record.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
@@ -37,16 +14,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, provide, ref, useTemplateRef } from 'vue'
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import { auth } from '/@/utils/common'
|
||||
|
||||
defineOptions({
|
||||
name: 'game/record',
|
||||
@@ -54,17 +29,7 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
|
||||
const settingsLoading = ref(false)
|
||||
const settingsSaving = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const autoCreate = ref(false)
|
||||
const manualCreate = ref(false)
|
||||
const settingsReady = ref(false)
|
||||
|
||||
const canSettings = computed(() => auth('recordSettings'))
|
||||
const canManual = computed(() => auth('createNextManual'))
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit'])
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/game.Record/'),
|
||||
@@ -112,92 +77,13 @@ const baTable = new baTableClass(
|
||||
|
||||
provide('baTable', baTable)
|
||||
|
||||
async function loadRecordSettings() {
|
||||
if (!canSettings.value) return
|
||||
settingsLoading.value = true
|
||||
try {
|
||||
const res = await createAxios({ url: '/admin/game.Record/recordSettings', method: 'get', showCodeMessage: false })
|
||||
if (res.code === 1 && res.data) {
|
||||
autoCreate.value = res.data.period_auto_create_enabled === 1
|
||||
manualCreate.value = res.data.period_manual_create_enabled === 1
|
||||
settingsReady.value = true
|
||||
}
|
||||
} finally {
|
||||
settingsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onSaveSettings() {
|
||||
if (!canSettings.value) return
|
||||
settingsSaving.value = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/game.Record/recordSettings',
|
||||
method: 'post',
|
||||
data: {
|
||||
period_auto_create_enabled: autoCreate.value ? 1 : 0,
|
||||
period_manual_create_enabled: manualCreate.value ? 1 : 0,
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
} finally {
|
||||
settingsSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSwitchChange() {
|
||||
if (!settingsReady.value) return
|
||||
void onSaveSettings()
|
||||
}
|
||||
|
||||
async function onCreateNextManual() {
|
||||
if (!canManual.value) return
|
||||
createLoading.value = true
|
||||
try {
|
||||
await createAxios({ url: '/admin/game.Record/createNextManual', method: 'post', showSuccessMessage: true })
|
||||
await baTable.getData()
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
void loadRecordSettings()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.record-settings-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.record-settings-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.record-setting-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.record-setting-label {
|
||||
min-width: 160px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.record-setting-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
.record-setting-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
|
||||
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'">
|
||||
<el-form v-if="!baTable.form.loading" ref="formRef" @submit.prevent="" @keyup.enter="baTable.onSubmit(formRef)" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules">
|
||||
<el-form v-if="!baTable.form.loading" ref="formRef" :model="baTable.form.items" :label-position="config.layout.shrink ? 'top' : 'right'" :label-width="baTable.form.labelWidth + 'px'" :rules="rules" :disabled="true">
|
||||
<FormItem :label="t('game.record.period_no')" type="string" v-model="baTable.form.items!.period_no" prop="period_no" />
|
||||
<FormItem :label="t('game.record.period_start_at')" type="datetime" v-model="baTable.form.items!.period_start_at" prop="period_start_at" />
|
||||
<FormItem
|
||||
@@ -33,9 +33,6 @@
|
||||
<template #footer>
|
||||
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
|
||||
<el-button @click="baTable.toggleForm()">{{ t('Cancel') }}</el-button>
|
||||
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
|
||||
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
Reference in New Issue
Block a user