重构DiceLotteryConfig为DiceLotteryPoolConfig

This commit is contained in:
2026-03-10 17:56:14 +08:00
parent 1a748745cb
commit 54aa0bd34f
22 changed files with 160 additions and 160 deletions

View File

@@ -0,0 +1,168 @@
<template>
<div class="art-full-height">
<!-- 搜索面板 -->
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
<ElCard class="art-table-card" shadow="never">
<!-- 表格头部 -->
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElButton
v-permission="'dice:lottery_pool_config:index:index'"
type="primary"
@click="showCurrentPoolDialog"
>
查看当前彩金池
</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
rowKey="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<!-- 操作列 -->
<template #operation="{ row }">
<div class="flex gap-2">
<SaButton
v-permission="'dice:lottery_pool_config:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<!-- <SaButton-->
<!-- v-permission="'dice:lottery_pool_config:index:destroy'"-->
<!-- type="error"-->
<!-- @click="deleteRow(row, api.delete, refreshData)"-->
<!-- />-->
</div>
</template>
</ArtTable>
</ElCard>
<!-- 编辑弹窗 -->
<EditDialog
v-model="dialogVisible"
:dialog-type="dialogType"
:data="dialogData"
@success="refreshData"
/>
<!-- 当前彩金池弹窗 -->
<CurrentPoolDialog v-model="currentPoolVisible" @success="refreshData" />
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/lottery_pool_config/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import CurrentPoolDialog from './modules/current-pool-dialog.vue'
// 搜索表单
const searchForm = ref({
name: undefined,
type: undefined
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 奖池类型展示0=正常 1=强制杀猪 2=T1高倍率
const typeFormatter = (row: Record<string, unknown>) =>
row.type === 0 ? '正常' : row.type === 1 ? '强制杀猪' : row.type === 2 ? 'T1高倍率' : '-'
// 权重列带 %
const weightFormatter = (prop: string) => (row: Record<string, unknown>) => {
const v = row[prop]
return v != null && v !== '' ? `${v}%` : '-'
}
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => [
{ prop: 'name', label: '名称', align: 'center' },
{ prop: 'type', label: '奖池类型', width: 100, align: 'center', formatter: typeFormatter },
{ prop: 'safety_line', label: '安全线', align: 'center' },
{
prop: 't1_weight',
label: 'T1池权重',
width: 100,
align: 'center',
formatter: weightFormatter('t1_weight')
},
{
prop: 't2_weight',
label: 'T2池权重',
width: 100,
align: 'center',
formatter: weightFormatter('t2_weight')
},
{
prop: 't3_weight',
label: 'T3池权重',
width: 100,
align: 'center',
formatter: weightFormatter('t3_weight')
},
{
prop: 't4_weight',
label: 'T4池权重',
width: 100,
align: 'center',
formatter: weightFormatter('t4_weight')
},
{
prop: 't5_weight',
label: 'T5池权重',
width: 100,
align: 'center',
formatter: weightFormatter('t5_weight')
},
{
prop: 'operation',
label: '操作',
width: 60,
align: 'center',
fixed: 'right',
useSlot: true
}
]
}
})
// 编辑配置
const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange } = useSaiAdmin()
const currentPoolVisible = ref(false)
function showCurrentPoolDialog() {
currentPoolVisible.value = true
}
</script>

View File

@@ -0,0 +1,236 @@
<template>
<el-dialog
v-model="visible"
title="当前彩金池"
width="560px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div v-if="loading && !pool" class="flex justify-center py-8">加载中...</div>
<template v-else-if="pool">
<div class="pool-info mb-4">
<div class="flex items-center gap-2 mb-3">
<span class="text-gray-500">池子名称</span>
<span>{{ pool.name }}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">池子盈利</span>
<span class="font-mono text-lg" :class="profitAmountClass">{{ displayProfitAmount }}</span>
<span class="text-gray-400 text-sm">实时不可修改</span>
</div>
</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="安全线" prop="safety_line">
<el-input-number
v-model="formData.safety_line"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="T1池权重(%)" prop="t1_weight">
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T2池权重(%)" prop="t2_weight">
<el-slider v-model="formData.t2_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T3池权重(%)" prop="t3_weight">
<el-slider v-model="formData.t3_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T4池权重(%)" prop="t4_weight">
<el-slider v-model="formData.t4_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T5池权重(%)" prop="t5_weight">
<el-slider v-model="formData.t5_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item>
<div class="text-gray-500 text-sm">
五个池权重总和<span :class="weightsSum !== 100 ? 'text-red-500' : ''">{{
weightsSum
}}</span
>% / 100%100%
</div>
</el-form-item>
</el-form>
</template>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" :loading="saving" :disabled="!pool" @click="handleSubmit">
保存权重与安全线
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '../../../api/lottery_pool_config/index'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface PoolData {
id: number
name: string
safety_line: number
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
profit_amount: number
}
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'success'): void }>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const loading = ref(false)
const saving = ref(false)
const pool = ref<PoolData | null>(null)
const formRef = ref<FormInstance>()
const formData = reactive({
safety_line: 0,
t1_weight: 0,
t2_weight: 0,
t3_weight: 0,
t4_weight: 0,
t5_weight: 0
})
const rules: FormRules = {
safety_line: [{ required: true, message: '请输入安全线', trigger: 'blur' }],
t1_weight: [{ required: true, message: '请输入T1权重', trigger: 'blur' }],
t2_weight: [{ required: true, message: '请输入T2权重', trigger: 'blur' }],
t3_weight: [{ required: true, message: '请输入T3权重', trigger: 'blur' }],
t4_weight: [{ required: true, message: '请输入T4权重', trigger: 'blur' }],
t5_weight: [{ required: true, message: '请输入T5权重', trigger: 'blur' }]
}
const weightsSum = computed(
() =>
formData.t1_weight +
formData.t2_weight +
formData.t3_weight +
formData.t4_weight +
formData.t5_weight
)
const displayProfitAmount = computed(() => {
const v = pool.value?.profit_amount
if (v == null || Number.isNaN(v)) return '-'
return Number(v).toFixed(2)
})
const profitAmountClass = computed(() => {
const v = pool.value?.profit_amount
if (v == null) return ''
if (v > 0) return 'text-green-600'
if (v < 0) return 'text-red-600'
return ''
})
let pollTimer: ReturnType<typeof setInterval> | null = null
const POLL_INTERVAL = 2000
async function loadPool() {
if (!visible.value) return
try {
loading.value = true
const res = await api.getCurrentPool()
const data = res as unknown as PoolData
if (data && typeof data === 'object') {
pool.value = data
formData.safety_line = data.safety_line ?? 0
formData.t1_weight = data.t1_weight ?? 0
formData.t2_weight = data.t2_weight ?? 0
formData.t3_weight = data.t3_weight ?? 0
formData.t4_weight = data.t4_weight ?? 0
formData.t5_weight = data.t5_weight ?? 0
}
} catch (e: any) {
ElMessage.error(e?.message ?? '获取彩金池失败')
} finally {
loading.value = false
}
}
function startPolling() {
stopPolling()
pollTimer = setInterval(() => {
if (!visible.value) {
stopPolling()
return
}
api.getCurrentPool().then((res) => {
const data = res as unknown as PoolData
if (pool.value && data && typeof data === 'object' && data.profit_amount != null) {
pool.value.profit_amount = data.profit_amount
}
})
}, POLL_INTERVAL)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
async function handleSubmit() {
if (!formRef.value || !pool.value) return
if (weightsSum.value !== 100) {
ElMessage.warning('T1T5 权重合计须为 100%')
return
}
try {
await formRef.value.validate()
saving.value = true
await api.updateCurrentPool({
safety_line: formData.safety_line,
t1_weight: formData.t1_weight,
t2_weight: formData.t2_weight,
t3_weight: formData.t3_weight,
t4_weight: formData.t4_weight,
t5_weight: formData.t5_weight
})
ElMessage.success('保存成功')
await loadPool()
emit('success')
} catch (e: any) {
if (e?.message) ElMessage.error(e.message)
} finally {
saving.value = false
}
}
function handleClose() {
stopPolling()
visible.value = false
pool.value = null
}
watch(
() => props.modelValue,
(open) => {
if (open) {
loadPool().then(() => startPolling())
} else {
stopPolling()
}
}
)
onUnmounted(() => stopPolling())
</script>
<style scoped>
.pool-info {
padding: 8px 0;
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<el-dialog
v-model="visible"
:title="dialogType === 'add' ? '新增色子奖池配置' : '编辑色子奖池配置'"
width="600px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入名称"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="奖池类型" prop="type">
<el-select
v-model="formData.type"
placeholder="请选择奖池类型"
clearable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option label="正常" :value="0" />
<el-option label="强制杀猪" :value="1" />
<el-option label="T1高倍率" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="安全线" prop="safety_line">
<el-input-number
v-model="formData.safety_line"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="T1池权重(%)" prop="t1_weight">
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T2池权重(%)" prop="t2_weight">
<el-slider v-model="formData.t2_weight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T3池权重(%)" prop="t3_weight">
<el-slider v-model="formData.t3_weight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T4池权重(%)" prop="t4_weight">
<el-slider v-model="formData.t4_weight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T5池权重(%)" prop="t5_weight">
<el-slider v-model="formData.t5_weight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item>
<div class="text-gray-500 text-sm">
五个池权重总和<span :class="Math.abs(weightsSum - 100) > 0.01 ? 'text-red-500' : ''">{{
weightsSum
}}</span
>% / 100%100%
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '../../../api/lottery_pool_config/index'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface Props {
modelValue: boolean
dialogType: string
data?: Record<string, any>
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dialogType: 'add',
data: undefined
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
/**
* 弹窗显示状态双向绑定
*/
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
/** 五个权重字段名,用于总和校验 */
const WEIGHT_KEYS = ['t1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'] as const
/** 五个池权重总和(用于展示与校验) */
const weightsSum = computed(() => {
return WEIGHT_KEYS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
})
/**
* 表单验证规则
*/
const rules = reactive<FormRules>({
name: [{ required: true, message: '名称必需填写', trigger: 'blur' }],
type: [{ required: true, message: '请选择奖池类型', trigger: 'change' }],
t1_weight: [{ required: true, message: 'T1池权重必需填写', trigger: 'blur' }],
t2_weight: [{ required: true, message: 'T2池权重必需填写', trigger: 'blur' }],
t3_weight: [{ required: true, message: 'T3池权重必需填写', trigger: 'blur' }],
t4_weight: [{ required: true, message: 'T4池权重必需填写', trigger: 'blur' }],
t5_weight: [{ required: true, message: 'T5池权重必需填写', trigger: 'blur' }]
})
/**
* 初始数据(权重为数字便于输入与校验)
*/
const initialFormData = {
id: null as number | null,
name: '',
remark: '',
type: null as number | null,
safety_line: 0 as number,
t1_weight: 0 as number,
t2_weight: 0 as number,
t3_weight: 0 as number,
t4_weight: 0 as number,
t5_weight: 0 as number
}
/**
* 表单数据
*/
const formData = reactive({ ...initialFormData })
/**
* 监听弹窗打开,初始化表单数据
*/
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
initPage()
}
}
)
/**
* 初始化页面数据
*/
const initPage = async () => {
// 先重置为初始值
Object.assign(formData, initialFormData)
// 如果有数据,则填充数据
if (props.data) {
await nextTick()
initForm()
}
}
/**
* 初始化表单数据(数值字段转为 number 便于滑块/输入框回显与校验)
*/
const initForm = () => {
if (!props.data) return
const numKeys = [
'id',
'type',
'safety_line',
't1_weight',
't2_weight',
't3_weight',
't4_weight',
't5_weight'
]
for (const key of Object.keys(formData)) {
if (!(key in props.data)) continue
const val = props.data[key]
if (numKeys.includes(key)) {
;(formData as any)[key] =
key === 'id' ? (val != null ? Number(val) || null : null) : Number(val) || 0
} else {
;(formData as any)[key] = val ?? ''
}
}
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (Math.abs(weightsSum.value - 100) > 0.01) {
ElMessage.warning('五个池权重总和必须为100%')
return
}
if (props.dialogType === 'add') {
await api.save(formData)
ElMessage.success('新增成功')
} else {
await api.update(formData)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,82 @@
<template>
<sa-search-bar
ref="searchBarRef"
v-model="formData"
label-width="100px"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
@expand="handleExpand"
>
<el-col v-bind="setSpan(6)">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="奖池类型" prop="type">
<el-select
v-model="formData.type"
:options="typeOptions"
placeholder="请选择奖池类型"
clearable
/>
</el-form-item>
</el-col>
</sa-search-bar>
</template>
<script setup lang="ts">
interface Props {
modelValue: Record<string, any>
}
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', params: Record<string, any>): void
(e: 'reset'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 展开/收起
const isExpanded = ref<boolean>(false)
const typeOptions = [
{ name: '0', value: '正常' },
{ name: '1', value: '强制杀猪' },
{ name: '2', value: 'T1高倍率' }
]
// 表单数据双向绑定
const searchBarRef = ref()
const formData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// 重置
function handleReset() {
searchBarRef.value?.ref.resetFields()
emit('reset')
}
// 搜索
async function handleSearch() {
emit('search', formData.value)
}
// 展开/收起
function handleExpand(expanded: boolean) {
isExpanded.value = expanded
}
// 栅格占据的列数
const setSpan = (span: number) => {
return {
span: span,
xs: 24, // 手机:满宽显示
sm: span >= 12 ? span : 12, // 平板大于等于12保持否则用半宽
md: span >= 8 ? span : 8, // 中等屏幕大于等于8保持否则用三分之一宽
lg: span,
xl: span
}
}
</script>