1.优化游戏记录审核

2.优化游戏配置页面
3.备份数据库
This commit is contained in:
2026-04-29 10:10:50 +08:00
parent ba4ece422a
commit e47857fcf2
8 changed files with 1607 additions and 117 deletions

View File

@@ -1,5 +1,25 @@
export default {
'quick Search Fields': 'ID / Key / Remark',
'form tab label': 'Game config',
'save success': 'Saved successfully',
'field period_auto_create_enabled': 'Auto create next period',
'field_tip period_auto_create_enabled': 'Whether to automatically create the next period',
'field period_manual_create_enabled': 'Manual create next period',
'field_tip period_manual_create_enabled': 'Whether manual creation of the next period is allowed',
'field period_seconds': 'Period duration (seconds)',
'field_tip period_seconds': 'Duration of each period in seconds',
'field bet_seconds': 'Betting duration (seconds)',
'field_tip bet_seconds': 'How many seconds betting stays open in each period',
'field pick_max_number_count': 'Max numbers per ticket',
'field_tip pick_max_number_count': 'Maximum amount of selectable numbers per ticket',
'field min_bet_per_number': 'Min bet per number',
'field_tip min_bet_per_number': 'Minimum bet amount per selected number',
'field max_bet_per_number': 'Max bet per number',
'field_tip max_bet_per_number': 'Maximum bet amount per selected number',
'field withdraw_bet_flow_ratio': 'Withdraw flow ratio',
'field_tip withdraw_bet_flow_ratio': 'Required betting flow ratio before withdrawal',
'field jackpot_max_amount': 'Jackpot review threshold',
'field_tip jackpot_max_amount': 'Winning amount threshold that requires manual jackpot review',
id: 'ID',
config_key: 'Config key',
config_value: 'Value',

View File

@@ -17,7 +17,7 @@ export default {
'status 1': 'Pending draw',
'status 2': 'Settled',
'status 3': 'Refunded',
'status 4': 'Returned',
'status 4': 'Returned (review rejected)',
'status 5': 'Pending review',
review_title: 'Win review',
review_open: 'Review',

View File

@@ -1,5 +1,25 @@
export default {
'quick Search Fields': 'ID/参数键/说明',
'form tab label': '游戏参数配置',
'save success': '保存成功',
'field period_auto_create_enabled': '自动创建下一期开关',
'field_tip period_auto_create_enabled': '是否允许自动创建下一期',
'field period_manual_create_enabled': '手动创建下一期开关',
'field_tip period_manual_create_enabled': '是否允许手动创建下一期',
'field period_seconds': '每期时长(秒)',
'field_tip period_seconds': '每一局的总时长(秒)',
'field bet_seconds': '下注时长(秒)',
'field_tip bet_seconds': '每一局允许下注的时长(秒)',
'field pick_max_number_count': '单注最多号码个数',
'field_tip pick_max_number_count': '单注最多可选号码数量',
'field min_bet_per_number': '单号最小下注额',
'field_tip min_bet_per_number': '每个号码允许的最小下注金额',
'field max_bet_per_number': '单号最大下注额',
'field_tip max_bet_per_number': '每个号码允许的最大下注金额',
'field withdraw_bet_flow_ratio': '提现所需流水倍数',
'field_tip withdraw_bet_flow_ratio': '提现前所需完成的投注流水倍数',
'field jackpot_max_amount': '大奖审核阈值',
'field_tip jackpot_max_amount': '达到该中奖金额后需要进入大奖审核流程',
id: 'ID',
config_key: '参数键',
config_value: '参数值',

View File

@@ -17,7 +17,7 @@ export default {
'status 1': '待开奖',
'status 2': '已结算',
'status 3': '已退款',
'status 4': '已退回',
'status 4': '已退回·拒审',
'status 5': '待审核',
review_title: '中奖审核',
review_open: '审核',

View File

@@ -1,134 +1,197 @@
<template>
<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 />
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.gameConfig.quick Search Fields') })"
></TableHeader>
<Table ref="tableRef"></Table>
<PopupForm />
<div class="default-main">
<el-row v-loading="state.loading">
<el-col :xs="24" :sm="18">
<el-alert v-if="state.remark" :title="state.remark" type="info" show-icon />
<el-form
v-if="!state.loading"
ref="formRef"
:model="state.form"
:label-position="'top'"
@submit.prevent=""
@keyup.enter="onSubmit()"
>
<el-tabs v-model="state.activeTab" type="border-card">
<el-tab-pane :label="t('config.gameConfig.form tab label')" name="game_config">
<div class="config-form-item" v-for="item in state.configList" :key="item.id">
<FormItem
:label="resolveFieldLabel(item.config_key)"
:type="resolveFormType(item.value_type)"
v-model="state.form[item.config_key]"
:input-attr="resolveInputAttr(item.value_type)"
:tip="resolveFieldTip(item.config_key, item.remark)"
/>
<div class="config-form-item-name">{{ item.config_key }}</div>
</div>
<el-button @click="onReset">{{ t('Reset') }}</el-button>
<el-button type="primary" :loading="state.submitLoading" @click="onSubmit()">{{ t('Save') }}</el-button>
</el-tab-pane>
</el-tabs>
</el-form>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { onMounted, reactive, 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 baTableClass from '/@/utils/baTable'
import FormItem from '/@/components/formItem/index.vue'
import createAxios from '/@/utils/axios'
defineOptions({
name: 'config/gameConfig',
})
const { t } = useI18n()
const tableRef = useTemplateRef('tableRef')
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
interface GameConfigItem {
id: number
config_key: string
config_value: string
value_type: 'string' | 'int' | 'decimal' | 'json'
remark: string
}
const baTable = new baTableClass(
new baTableApi('/admin/config.GameConfig/'),
{
pk: 'id',
filter: { page: 1, limit: 50 },
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('config.gameConfig.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
{
label: t('config.gameConfig.config_key'),
prop: 'config_key',
align: 'center',
minWidth: 200,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.config_value'),
prop: 'config_value',
align: 'center',
minWidth: 160,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.value_type'),
prop: 'value_type',
align: 'center',
width: 110,
operator: 'eq',
render: 'tag',
custom: {
string: 'primary',
int: 'success',
decimal: 'warning',
json: 'info',
},
replaceValue: {
string: t('config.gameConfig.value_type string'),
int: t('config.gameConfig.value_type int'),
decimal: t('config.gameConfig.value_type decimal'),
json: t('config.gameConfig.value_type json'),
},
},
{
label: t('config.gameConfig.remark'),
prop: 'remark',
align: 'center',
minWidth: 200,
operatorPlaceholder: t('Fuzzy query'),
operator: 'LIKE',
showOverflowTooltip: true,
},
{
label: t('config.gameConfig.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{
label: t('config.gameConfig.update_time'),
prop: 'update_time',
align: 'center',
render: 'datetime',
operator: 'RANGE',
comSearchRender: 'datetime',
sortable: 'custom',
width: 170,
timeFormat: 'yyyy-mm-dd hh:MM:ss',
},
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
],
dblClickNotEditColumn: [undefined],
},
{
defaultItems: { value_type: 'string', remark: '' },
const { t, te } = useI18n()
const formRef = useTemplateRef('formRef')
const api = new baTableApi('/admin/config.GameConfig/')
const excludedConfigKeys = new Set(['period_auto_create_enabled', 'period_manual_create_enabled'])
const state: {
loading: boolean
submitLoading: boolean
activeTab: string
remark: string
configList: GameConfigItem[]
form: Record<string, string | number>
} = reactive({
loading: true,
submitLoading: false,
activeTab: 'game_config',
remark: '',
configList: [],
form: {},
})
const getData = () => {
state.loading = true
api.index({ page: 1, limit: 999 }).then((res) => {
const allList = (res.data.list || []) as GameConfigItem[]
const list = allList.filter((item) => !excludedConfigKeys.has(item.config_key))
state.configList = list
state.remark = res.data.remark || ''
const nextForm: Record<string, string | number> = {}
for (const item of list) {
if (item.value_type === 'int' || item.value_type === 'decimal') {
const parsed = Number(item.config_value)
nextForm[item.config_key] = Number.isNaN(parsed) ? 0 : parsed
continue
}
nextForm[item.config_key] = item.config_value ?? ''
}
state.form = nextForm
}).finally(() => {
state.loading = false
})
}
const resolveFormType = (valueType: GameConfigItem['value_type']) => {
if (valueType === 'json') return 'textarea'
if (valueType === 'int' || valueType === 'decimal') return 'number'
return 'string'
}
const resolveInputAttr = (valueType: GameConfigItem['value_type']) => {
if (valueType === 'json') {
return { rows: 5 }
}
)
if (valueType === 'decimal') {
return { precision: 4 }
}
return {}
}
provide('baTable', baTable)
const resolveFieldLabel = (configKey: string) => {
const langKey = `config.gameConfig.field ${configKey}`
if (te(langKey)) {
return t(langKey)
}
return configKey
}
const resolveFieldTip = (configKey: string, fallbackRemark: string) => {
const langKey = `config.gameConfig.field_tip ${configKey}`
if (te(langKey)) {
return t(langKey)
}
return fallbackRemark || ''
}
const onSubmit = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
state.submitLoading = true
try {
await createAxios({
url: '/admin/config.GameConfig/save',
method: 'post',
data: {
items: JSON.parse(JSON.stringify(state.form)),
},
showSuccessMessage: true,
})
getData()
} finally {
state.submitLoading = false
}
}
const onReset = () => {
getData()
}
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.initSort()
baTable.dragSort()
})
getData()
})
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.el-tabs--border-card {
border: none;
box-shadow: var(--el-box-shadow-light);
border-radius: var(--el-border-radius-base);
}
.el-tabs--border-card :deep(.el-tabs__header) {
background-color: var(--ba-bg-color);
border-bottom: none;
border-top-left-radius: var(--el-border-radius-base);
border-top-right-radius: var(--el-border-radius-base);
}
.el-tabs--border-card :deep(.el-tabs__item.is-active) {
border: 1px solid transparent;
}
.el-tabs--border-card :deep(.el-tabs__nav-wrap) {
border-top-left-radius: var(--el-border-radius-base);
border-top-right-radius: var(--el-border-radius-base);
}
.config-form-item {
display: flex;
align-items: center;
.el-form-item {
flex: 13;
}
.config-form-item-name {
flex: 3;
font-size: 13px;
color: var(--el-text-color-disabled);
padding-left: 20px;
opacity: 0;
}
&:hover .config-form-item-name {
opacity: 1;
}
}
</style>

View File

@@ -19,7 +19,12 @@
<el-descriptions-item :label="t('game.playRecord.pick_numbers')">{{ formatPickNumbers({}, {}, reviewDialog.row?.pick_numbers) }}</el-descriptions-item>
<el-descriptions-item :label="t('game.playRecord.total_amount')">{{ formatAmount({}, {}, reviewDialog.row?.total_amount) }}</el-descriptions-item>
<el-descriptions-item :label="t('game.playRecord.win_amount')">{{ formatAmount({}, {}, reviewDialog.row?.win_amount) }}</el-descriptions-item>
<el-descriptions-item :label="t('game.playRecord.status')">{{ reviewDialog.row?.status ? t(`game.playRecord.status ${reviewDialog.row.status}`) : '-' }}</el-descriptions-item>
<el-descriptions-item :label="t('game.playRecord.status')">
<el-tag v-if="reviewDialog.row?.status !== null && reviewDialog.row?.status !== undefined && reviewDialog.row?.status !== ''" :type="playRecordStatusTagType(reviewDialog.row.status)" effect="dark" size="small">
{{ t(`game.playRecord.status ${reviewDialog.row.status}`) }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<el-form label-width="90px">
<el-form-item :label="t('game.playRecord.review_remark')">
@@ -81,6 +86,18 @@ function formatPickNumbers(_row: anyObj, _column: any, cellValue: unknown) {
}
}
function playRecordStatusTagType(status: unknown): 'primary' | 'success' | 'info' | 'warning' | 'danger' {
const key = status === null || status === undefined ? '' : String(status)
const map: Record<string, 'primary' | 'success' | 'info' | 'warning' | 'danger'> = {
'1': 'info',
'2': 'success',
'3': 'danger',
'4': 'warning',
'5': 'primary',
}
return map[key] ?? 'info'
}
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
if (cellValue === null || cellValue === undefined || cellValue === '') {
return '-'
@@ -164,11 +181,11 @@ const baTable = new baTableClass(
label: t('game.playRecord.status'),
prop: 'status',
align: 'center',
width: 100,
minWidth: 118,
operator: 'eq',
render: 'tag',
effect: 'dark',
custom: { '1': 'warning', '2': 'success', '3': 'danger', '4': 'info', '5': 'warning' },
custom: { '1': 'info', '2': 'success', '3': 'danger', '4': 'warning', '5': 'primary' },
replaceValue: {
'1': t('game.playRecord.status 1'),
'2': t('game.playRecord.status 2'),