Compare commits

...

13 Commits

50 changed files with 5613 additions and 6 deletions

View File

@@ -14,12 +14,6 @@
<ElCol :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="action-column"> <ElCol :xs="24" :sm="24" :md="6" :lg="6" :xl="6" class="action-column">
<div class="action-buttons-wrapper" :style="actionButtonsStyle"> <div class="action-buttons-wrapper" :style="actionButtonsStyle">
<div class="form-buttons"> <div class="form-buttons">
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:reset-right-line" />
</template>
{{ t('table.searchBar.reset') }}
</ElButton>
<ElButton <ElButton
v-if="showSearch" v-if="showSearch"
type="primary" type="primary"
@@ -33,6 +27,12 @@
</template> </template>
{{ t('table.searchBar.search') }} {{ t('table.searchBar.search') }}
</ElButton> </ElButton>
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:reset-right-line" />
</template>
{{ t('table.searchBar.reset') }}
</ElButton>
</div> </div>
<div v-if="showExpand" class="filter-toggle" @click="toggleExpand"> <div v-if="showExpand" class="filter-toggle" @click="toggleExpand">
<span>{{ expandToggleText }}</span> <span>{{ expandToggleText }}</span>

View File

@@ -0,0 +1,65 @@
import request from '@/utils/http'
/**
* 色子奖池配置 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/lottery_config/DiceLotteryConfig/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/lottery_config/DiceLotteryConfig/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/lottery_config/DiceLotteryConfig/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/lottery_config/DiceLotteryConfig/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/lottery_config/DiceLotteryConfig/destroy',
data: params
})
}
}

View File

@@ -0,0 +1,86 @@
import request from '@/utils/http'
/**
* 玩家抽奖记录 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/play_record/DicePlayRecord/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/play_record/DicePlayRecord/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/play_record/DicePlayRecord/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/play_record/DicePlayRecord/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/play_record/DicePlayRecord/destroy',
data: params
})
},
/** 获取玩家选项id、username */
getPlayerOptions() {
return request.get<{ id: number; username: string }[]>({
url: '/dice/play_record/DicePlayRecord/getPlayerOptions'
})
},
/** 获取彩金池配置选项id、name */
getLotteryConfigOptions() {
return request.get<{ id: number; name: string }[]>({
url: '/dice/play_record/DicePlayRecord/getLotteryConfigOptions'
})
},
/** 获取奖励配置选项id、ui_text、tier */
getRewardConfigOptions() {
return request.get<{ id: number; ui_text: string; tier: string }[]>({
url: '/dice/play_record/DicePlayRecord/getRewardConfigOptions'
})
}
}

View File

@@ -0,0 +1,75 @@
import request from '@/utils/http'
/**
* 大富翁-玩家 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/player/DicePlayer/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/player/DicePlayer/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/player/DicePlayer/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/player/DicePlayer/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/player/DicePlayer/destroy',
data: params
})
},
/**
* 仅更新状态(列表内开关用)
*/
updateStatus(params: { id: number | string; status: number }) {
return request.put<any>({
url: '/dice/player/DicePlayer/updateStatus',
data: params
})
}
}

View File

@@ -0,0 +1,74 @@
import request from '@/utils/http'
/**
* 玩家购买抽奖记录 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/destroy',
data: params
})
},
/**
* 获取玩家选项id、username用于下拉
*/
getPlayerOptions() {
return request.get<Api.Common.ApiData>({
url: '/dice/player_coin_record/DicePlayerCoinRecord/getPlayerOptions'
})
}
}

View File

@@ -0,0 +1,84 @@
import request from '@/utils/http'
/**
* 玩家钱包流水 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/destroy',
data: params
})
},
/**
* 获取玩家选项id、username用于下拉
*/
getPlayerOptions() {
return request.get<{ id: number; username: string }[]>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerOptions'
})
},
/**
* 获取指定玩家当前平台币(钱包操作前)
*/
getPlayerWalletBefore(playerId: number | string) {
return request.get<{ wallet_before: number }>({
url: '/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerWalletBefore',
params: { player_id: playerId }
})
}
}

View File

@@ -0,0 +1,65 @@
import request from '@/utils/http'
/**
* 奖励配置 API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/reward_config/DiceRewardConfig/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/reward_config/DiceRewardConfig/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/reward_config/DiceRewardConfig/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/reward_config/DiceRewardConfig/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/reward_config/DiceRewardConfig/destroy',
data: params
})
}
}

View File

@@ -0,0 +1,149 @@
<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>
<ElSpace wrap>
<ElButton
v-permission="'dice:lottery_config:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:lottery_config:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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_config:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:lottery_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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/lottery_config/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-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: () => [
{ type: 'selection' },
{ prop: 'name', label: '名称' },
{ prop: 'type', label: '奖池类型', width: 100, formatter: typeFormatter },
{ prop: 'safety_line', label: '安全线' },
{ prop: 't1_wight', label: 'T1池权重', width: 100, formatter: weightFormatter('t1_wight') },
{ prop: 't2_wight', label: 'T2池权重', width: 100, formatter: weightFormatter('t2_wight') },
{ prop: 't3_wight', label: 'T3池权重', width: 100, formatter: weightFormatter('t3_wight') },
{ prop: 't4_wight', label: 'T4池权重', width: 100, formatter: weightFormatter('t4_wight') },
{ prop: 't5_wight', label: 'T5池权重', width: 100, formatter: weightFormatter('t5_wight') },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,235 @@
<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="请输入名称" />
</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%"
>
<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_wight">
<el-slider v-model="formData.t1_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T2池权重(%)" prop="t2_wight">
<el-slider v-model="formData.t2_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T3池权重(%)" prop="t3_wight">
<el-slider v-model="formData.t3_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T4池权重(%)" prop="t4_wight">
<el-slider v-model="formData.t4_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T5池权重(%)" prop="t5_wight">
<el-slider v-model="formData.t5_wight" :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_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_wight', 't2_wight', 't3_wight', 't4_wight', 't5_wight'] 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_wight: [{ required: true, message: 'T1池权重必需填写', trigger: 'blur' }],
t2_wight: [{ required: true, message: 'T2池权重必需填写', trigger: 'blur' }],
t3_wight: [{ required: true, message: 'T3池权重必需填写', trigger: 'blur' }],
t4_wight: [{ required: true, message: 'T4池权重必需填写', trigger: 'blur' }],
t5_wight: [{ 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_wight: 0 as number,
t2_wight: 0 as number,
t3_wight: 0 as number,
t4_wight: 0 as number,
t5_wight: 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_wight',
't2_wight',
't3_wight',
't4_wight',
't5_wight'
]
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>

View File

@@ -0,0 +1,182 @@
<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>
<ElSpace wrap>
<ElButton
v-permission="'dice:play_record:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:play_record:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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"
>
<!-- 彩金池配置 tag -->
<template #lottery_config_id="{ row }">
<ElTag size="small">{{ lotteryConfigNameFormatter(row) }}</ElTag>
</template>
<!-- 抽奖类型 tag -->
<template #lottery_type="{ row }">
<ElTag size="small" :type="row.lottery_type === 0 ? 'warning' : 'success'">
{{ row.lottery_type === 0 ? '付费' : row.lottery_type === 1 ? '赠送' : '-' }}
</ElTag>
</template>
<!-- 中奖 tag -->
<template #is_win="{ row }">
<ElTag size="small" :type="row.is_win === 1 ? 'success' : 'info'">
{{ row.is_win === 0 ? '无' : row.is_win === 1 ? '中奖' : '-' }}
</ElTag>
</template>
<!-- 操作列 -->
<template #operation="{ row }">
<div class="flex gap-2">
<SaButton
v-permission="'dice:play_record:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:play_record: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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/play_record/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
// 搜索表单
const searchForm = ref<Record<string, unknown>>({
username: undefined,
lottery_config_name: undefined,
lottery_type: undefined,
is_win: undefined,
win_coin_min: undefined,
win_coin_max: undefined,
reward_ui_text: undefined,
reward_tier: undefined
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
const usernameFormatter = (row: Record<string, any>) =>
row?.dicePlayer?.username ?? row?.player_id ?? '-'
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
row?.diceLotteryConfig?.name ?? row?.lottery_config_id ?? '-'
const rewardTierFormatter = (row: Record<string, any>) =>
row?.diceRewardConfig?.tier ?? row?.reward_config_id ?? '-'
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => [
{ type: 'selection' },
{ prop: 'id', label: 'ID', width: 80 },
{
prop: 'player_id',
label: '玩家',
formatter: (row: Record<string, any>) => usernameFormatter(row)
},
{
prop: 'lottery_config_id',
label: '彩金池配置',
width: 120,
useSlot: true
},
{ prop: 'lottery_type', label: '抽奖类型', width: 100, useSlot: true },
{ prop: 'is_win', label: '中奖', width: 80, useSlot: true },
{ prop: 'win_coin', label: '赢取平台币' },
{
prop: 'reward_config_id',
label: '奖励配置',
formatter: (row: Record<string, any>) => rewardTierFormatter(row)
},
{ prop: 'create_time', label: '创建时间', width: 170 },
{ prop: 'update_time', label: '修改时间', width: 170 },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,241 @@
<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="player_id">
<el-select
v-model="formData.player_id"
placeholder="请选择玩家(显示用户名)"
clearable
filterable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option
v-for="item in playerOptions"
:key="item.id"
:label="item.username"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="彩金池配置" prop="lottery_config_id">
<el-select
v-model="formData.lottery_config_id"
placeholder="请选择彩金池配置"
clearable
filterable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option
v-for="item in lotteryConfigOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="抽奖类型" prop="lottery_type">
<el-select
v-model="formData.lottery_type"
placeholder="请选择"
clearable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option label="付费" :value="0" />
<el-option label="赠送" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="中奖" prop="is_win">
<el-select
v-model="formData.is_win"
placeholder="请选择"
clearable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option label="无" :value="0" />
<el-option label="中奖" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="赢取平台币" prop="win_coin">
<el-input-number
v-model="formData.win_coin"
placeholder="请输入赢取平台币"
:precision="2"
style="width: 100%"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item label="奖励配置" prop="reward_config_id">
<el-select
v-model="formData.reward_config_id"
placeholder="请选择奖励配置(显示前端文本)"
clearable
filterable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option
v-for="item in rewardConfigOptions"
:key="item.id"
:label="
item.ui_text
? `${item.ui_text}${item.tier ? ' (' + item.tier + ')' : ''}`
: String(item.id)
"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">{{ dialogType === 'edit' ? '关闭' : '取消' }}</el-button>
<el-button v-if="dialogType === 'add'" type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '../../../api/play_record/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 rules = reactive<FormRules>({
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
lottery_config_id: [{ required: true, message: '请选择彩金池配置', trigger: 'change' }],
lottery_type: [{ required: true, message: '请选择抽奖类型', trigger: 'change' }],
is_win: [{ required: true, message: '请选择中奖', trigger: 'change' }],
win_coin: [{ required: true, message: '赢取平台币必填', trigger: 'blur' }],
reward_config_id: [{ required: true, message: '请选择奖励配置', trigger: 'change' }]
})
const playerOptions = ref<Array<{ id: number; username: string }>>([])
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
const rewardConfigOptions = ref<Array<{ id: number; ui_text: string; tier: string }>>([])
const initialFormData = {
id: null as number | null,
player_id: null as number | null,
lottery_config_id: null as number | null,
lottery_type: null as number | null,
is_win: null as number | null,
win_coin: null as number | null,
reward_config_id: null as number | null
}
const formData = reactive({ ...initialFormData })
watch(
() => props.modelValue,
async (open) => {
if (open) {
initPage()
try {
const [players, lotteryConfigs, rewardConfigs] = await Promise.all([
api.getPlayerOptions(),
api.getLotteryConfigOptions(),
api.getRewardConfigOptions()
])
playerOptions.value = Array.isArray(players) ? players : ((players as any)?.data ?? [])
lotteryConfigOptions.value = Array.isArray(lotteryConfigs)
? lotteryConfigs
: ((lotteryConfigs as any)?.data ?? [])
rewardConfigOptions.value = Array.isArray(rewardConfigs)
? rewardConfigs
: ((rewardConfigs as any)?.data ?? [])
} catch {
playerOptions.value = []
lotteryConfigOptions.value = []
rewardConfigOptions.value = []
}
}
}
)
const initPage = async () => {
Object.assign(formData, { ...initialFormData })
if (props.data) {
await nextTick()
initForm()
}
}
const initForm = () => {
if (!props.data) return
const keys = [
'id',
'player_id',
'lottery_config_id',
'lottery_type',
'is_win',
'win_coin',
'reward_config_id'
]
keys.forEach((key) => {
const val = props.data![key]
if (val != null && val !== undefined) {
;(formData as Record<string, unknown>)[key] = val
}
})
}
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (props.dialogType === 'add') {
const rest = { ...formData } as Record<string, unknown>
delete rest.id
await api.save(rest)
ElMessage.success('新增成功')
} else {
await api.update(formData)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<sa-search-bar
ref="searchBarRef"
v-model="formData"
label-width="120px"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
@expand="handleExpand"
>
<el-col v-bind="setSpan(6)">
<el-form-item label="玩家" prop="username">
<el-input v-model="formData.username" placeholder="用户名模糊" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="彩金池配置" prop="lottery_config_name">
<el-input v-model="formData.lottery_config_name" placeholder="名称模糊" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="抽奖类型" prop="lottery_type">
<el-select v-model="formData.lottery_type" placeholder="全部" clearable style="width: 100%">
<el-option label="付费" :value="0" />
<el-option label="赠送" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="中奖" prop="is_win">
<el-select v-model="formData.is_win" placeholder="全部" clearable style="width: 100%">
<el-option label="无" :value="0" />
<el-option label="中奖" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="赢取平台币" prop="win_coin_min">
<div class="range-wrap">
<el-input-number
v-model="formData.win_coin_min"
placeholder="最小"
:precision="2"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.win_coin_max"
placeholder="最大"
:precision="2"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="奖励配置" prop="reward_ui_text">
<el-input v-model="formData.reward_ui_text" placeholder="前端显示文本模糊" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="中奖名(档位)" prop="reward_tier">
<el-select v-model="formData.reward_tier" placeholder="全部" clearable style="width: 100%">
<el-option label="T1" value="T1" />
<el-option label="T2" value="T2" />
<el-option label="T3" value="T3" />
<el-option label="T4" value="T4" />
<el-option label="T5" value="T5" />
</el-select>
</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 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) => ({
span,
xs: 24,
sm: span >= 12 ? span : 12,
md: span >= 8 ? span : 8,
lg: span,
xl: span
})
</script>
<style lang="scss" scoped>
.range-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.range-input {
flex: 1;
min-width: 0;
}
.range-sep {
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,179 @@
<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>
<ElSpace wrap>
<ElButton v-permission="'dice:player:index:save'" @click="showDialog('add')" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:player:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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 #status="{ row }">
<ElSwitch
v-permission="'dice:player:index:update'"
:model-value="row.status === 1"
:loading="row._statusLoading"
@change="(v: boolean) => handleStatusChange(row, v ? 1 : 0)"
/>
</template>
<!-- 平台币tag 展示 -->
<template #coin="{ row }">
<ElTag type="info" size="small">{{ row.coin ?? 0 }}</ElTag>
</template>
<!-- 操作列 -->
<template #operation="{ row }">
<div class="flex gap-2">
<SaButton
v-permission="'dice:player:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:player: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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/player/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
// 搜索表单
const searchForm = ref({
username: undefined,
name: undefined,
status: undefined,
coin: undefined,
is_up: undefined
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 权重列带 % 的 formatter
const weightFormatter = (_row: any, _column: any, cellValue: unknown) =>
cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
// 倍率列展示0=正常 1=强制杀猪 2=T1高倍率
const isUpFormatter = (_row: any, _column: any, cellValue: unknown) =>
cellValue === 0 ? '正常' : cellValue === 1 ? '强制杀猪' : cellValue === 2 ? 'T1高倍率' : '-'
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => [
{ type: 'selection' },
{ prop: 'username', label: '用户名' },
{ prop: 'name', label: '昵称' },
{ prop: 'status', label: '状态', width: 88, useSlot: true },
{ prop: 'coin', label: '平台币', width: 100, useSlot: true },
{ prop: 'is_up', label: '倍率', width: 80, formatter: isUpFormatter },
{ prop: 't1_wight', label: 'T1池权重', width: 100, formatter: weightFormatter },
{ prop: 't2_wight', label: 'T2池权重', width: 100, formatter: weightFormatter },
{ prop: 't3_wight', label: 'T3池权重', width: 100, formatter: weightFormatter },
{ prop: 't4_wight', label: 'T4池权重', width: 100, formatter: weightFormatter },
{ prop: 't5_wight', label: 'T5池权重', width: 100, formatter: weightFormatter },
{ prop: 'total_draw_count', label: '总抽奖次数' },
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
{ prop: 'created_at', label: '创建时间' },
{ prop: 'updated_at', label: '更新时间' },
{ prop: 'operation', label: '操作', width: 120, fixed: 'right', useSlot: true }
]
}
})
// 状态开关切换(列表内直接修改)
const handleStatusChange = async (row: Record<string, any>, status: number) => {
row._statusLoading = true
try {
await api.updateStatus({ id: row.id, status })
row.status = status
} catch {
refreshData()
} finally {
row._statusLoading = false
}
}
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,216 @@
<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="username">
<el-input v-model="formData.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="昵称" prop="name">
<el-input v-model="formData.name" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="密码" prop="password" :rules="passwordRules">
<el-input
v-model="formData.password"
type="password"
placeholder="编辑留空则不修改"
show-password
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<sa-switch v-model="formData.status" />
</el-form-item>
<el-form-item label="平台币" prop="coin">
<el-input-number
v-model="formData.coin"
:min="0"
:precision="2"
:disabled="dialogType === 'add'"
placeholder="创建时默认0不可改"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="倍率" prop="is_up">
<el-select v-model="formData.is_up" placeholder="请选择倍率" clearable style="width: 100%">
<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="T1池权重(%)" prop="t1_wight">
<el-slider v-model="formData.t1_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T2池权重(%)" prop="t2_wight">
<el-slider v-model="formData.t2_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T3池权重(%)" prop="t3_wight">
<el-slider v-model="formData.t3_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T4池权重(%)" prop="t4_wight">
<el-slider v-model="formData.t4_wight" :min="0" :max="100" :step="0.01" show-input />
</el-form-item>
<el-form-item label="T5池权重(%)" prop="t5_wight">
<el-slider v-model="formData.t5_wight" :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/player/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_wight', 't2_wight', 't3_wight', 't4_wight', 't5_wight'] as const
const weightsSum = computed(() => {
return WEIGHT_KEYS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
})
/** 新增时密码必填,编辑时选填 */
const passwordRules = computed(() =>
props.dialogType === 'add' ? [{ required: true, message: '密码必需填写', trigger: 'blur' }] : []
)
const rules = reactive<FormRules>({
username: [{ required: true, message: '用户名必需填写', trigger: 'blur' }],
name: [{ required: true, message: '昵称必需填写', trigger: 'blur' }],
status: [{ required: true, message: '状态必需填写', trigger: 'blur' }],
coin: [{ required: true, message: '平台币必需填写', trigger: 'blur' }]
})
const initialFormData = {
id: null as number | null,
username: '',
name: '',
password: '',
status: 1 as number,
coin: 0 as number,
is_up: null as number | null,
t1_wight: 0 as number,
t2_wight: 0 as number,
t3_wight: 0 as number,
t4_wight: 0 as number,
t5_wight: 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()
}
}
const numKeys = [
'id',
'status',
'coin',
'is_up',
't1_wight',
't2_wight',
't3_wight',
't4_wight',
't5_wight'
]
const initForm = () => {
if (!props.data) return
for (const key of Object.keys(formData)) {
if (!(key in props.data)) continue
if (key === 'password') {
;(formData as any).password = ''
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
}
const payload = { ...formData }
if (props.dialogType === 'edit' && !payload.password) {
delete (payload as any).password
}
if (props.dialogType === 'add') {
await api.save(payload)
ElMessage.success('新增成功')
} else {
await api.update(payload)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,93 @@
<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="username">
<el-input v-model="formData.username" placeholder="请输入用户名" clearable />
</el-form-item>
</el-col>
<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="status">
<el-select v-model="formData.status" placeholder="全部" clearable style="width: 100%">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="平台币" prop="coin">
<el-input-number
v-model="formData.coin"
:min="0"
:precision="2"
placeholder="精确搜索"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="倍率" prop="is_up">
<el-select v-model="formData.is_up" placeholder="全部" clearable style="width: 100%">
<el-option label="正常" :value="0" />
<el-option label="强制杀猪" :value="1" />
<el-option label="T1高倍率" :value="2" />
</el-select>
</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 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) => ({
span,
xs: 24,
sm: span >= 12 ? span : 12,
md: span >= 8 ? span : 8,
lg: span,
xl: span
})
</script>

View File

@@ -0,0 +1,157 @@
<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>
<ElSpace wrap>
<ElButton v-permission="'dice:player_coin_record:index:save'" @click="showDialog('add')" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:player_coin_record:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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:player_coin_record:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:player_coin_record: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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/player_coin_record/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
// 搜索表单
const searchForm = ref<Record<string, unknown>>({
username: undefined,
use_coins_min: undefined,
use_coins_max: undefined,
total_draw_count_min: undefined,
total_draw_count_max: undefined,
paid_draw_count_min: undefined,
paid_draw_count_max: undefined,
free_draw_count_min: undefined,
free_draw_count_max: undefined,
create_time_min: undefined,
create_time_max: undefined,
create_time: undefined as [string, string] | undefined
})
// 搜索处理(将创建时间范围 create_time 转为 create_time_min / create_time_max
const handleSearch = (params: Record<string, any>) => {
const p = { ...params }
if (Array.isArray(p.create_time) && p.create_time.length === 2) {
p.create_time_min = p.create_time[0]
p.create_time_max = p.create_time[1]
}
delete p.create_time
Object.assign(searchParams, p)
getData()
}
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => {
const usernameFormatter = (row: Record<string, any>) =>
row?.dicePlayer?.username ?? row?.player_id ?? '-'
return [
{ type: 'selection' },
{ prop: 'id', label: 'ID', width: 80 },
{ prop: 'player_id', label: '玩家用户名', formatter: (row: Record<string, any>) => usernameFormatter(row) },
{ prop: 'use_coins', label: '消耗硬币' },
{ prop: 'total_draw_count', label: '总抽奖次数' },
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
{ prop: 'remark', label: '备注', width: 100, showOverflowTooltip: true },
{ prop: 'create_time', label: '创建时间', width: 170 },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,235 @@
<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="player_id">
<el-select
v-model="formData.player_id"
placeholder="请选择玩家(显示用户名)"
clearable
filterable
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<el-option
v-for="item in playerOptions"
:key="item.id"
:label="item.username"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="消耗硬币" prop="use_coins">
<el-input-number v-model="formData.use_coins" placeholder="请输入消耗硬币" :min="0" />
</el-form-item>
<el-form-item label="购买抽奖次数" prop="paid_draw_count">
<el-input-number
v-model="formData.paid_draw_count"
placeholder="请输入购买抽奖次数"
:min="0"
@change="onDrawCountChange"
/>
</el-form-item>
<el-form-item label="赠送抽奖次数" prop="free_draw_count">
<el-input-number
v-model="formData.free_draw_count"
placeholder="请输入赠送抽奖次数"
:min="0"
@change="onDrawCountChange"
/>
</el-form-item>
<el-form-item label="总抽奖次数" prop="total_draw_count">
<el-input-number
:model-value="totalDrawCountComputed"
placeholder="自动求和"
:min="0"
disabled
/>
</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>
<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/player_coin_record/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 rules = reactive<FormRules>({
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
use_coins: [{ required: true, message: '消耗硬币必需填写', trigger: 'blur' }],
paid_draw_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
free_draw_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
remark: [{ required: true, message: '备注必需填写', trigger: 'blur' }]
})
/** 玩家下拉选项id、username */
const playerOptions = ref<Array<{ id: number; username: string }>>([])
/** 总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数(只读展示) */
const totalDrawCountComputed = computed(() => {
const paid = Number(formData.paid_draw_count) || 0
const free = Number(formData.free_draw_count) || 0
return paid + free
})
function onDrawCountChange() {
formData.total_draw_count = totalDrawCountComputed.value
}
/**
* 初始数据
*/
const initialFormData = {
id: null,
player_id: null,
use_coins: null as number | null,
total_draw_count: null as number | null,
paid_draw_count: null as number | null,
free_draw_count: null as number | null,
remark: ''
}
/**
* 表单数据
*/
const formData = reactive({ ...initialFormData })
/**
* 监听弹窗打开,初始化表单并拉取玩家选项(与 player_wallet_record 一致)
*/
watch(
() => props.modelValue,
async (open) => {
if (open) {
initPage()
try {
const list = await api.getPlayerOptions()
const arr = Array.isArray(list) ? list : (list as any)?.data
playerOptions.value = Array.isArray(arr)
? (arr as Array<{ id: number; username: string }>)
: []
} catch {
playerOptions.value = []
}
}
}
)
/**
* 初始化页面数据(仅重置表单、回填编辑数据,不在此处请求玩家列表)
*/
const initPage = async () => {
Object.assign(formData, { ...initialFormData })
if (props.data) {
await nextTick()
initForm()
}
}
/**
* 初始化表单数据
*/
const initForm = () => {
if (!props.data) return
const keys = [
'id',
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'remark'
]
keys.forEach((key) => {
const val = props.data![key]
if (val != null && val !== undefined) {
;(formData as Record<string, unknown>)[key] = val
}
})
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单(总抽奖次数由购买+赠送自动求和,提交前写入)
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
formData.total_draw_count = totalDrawCountComputed.value
await formRef.value.validate()
if (props.dialogType === 'add') {
const rest = { ...formData } as Record<string, unknown>
delete rest.id
await api.save(rest)
ElMessage.success('新增成功')
} else {
await api.update(formData)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,184 @@
<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="username">
<el-input v-model="formData.username" placeholder="按用户名搜索" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="消耗硬币" prop="use_coins_min">
<div class="range-wrap">
<el-input-number
v-model="formData.use_coins_min"
placeholder="最小"
:min="0"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.use_coins_max"
placeholder="最大"
:min="0"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="总抽奖次数" prop="total_draw_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.total_draw_count_min"
placeholder="最小"
:min="0"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.total_draw_count_max"
placeholder="最大"
:min="0"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="购买抽奖次数" prop="paid_draw_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.paid_draw_count_min"
placeholder="最小"
:min="0"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.paid_draw_count_max"
placeholder="最大"
:min="0"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="赠送抽奖次数" prop="free_draw_count_min">
<div class="range-wrap">
<el-input-number
v-model="formData.free_draw_count_min"
placeholder="最小"
:min="0"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.free_draw_count_max"
placeholder="最大"
:min="0"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(8)">
<el-form-item label="创建时间" prop="create_time_min">
<el-date-picker
v-model="formData.create_time"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
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 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>
<style lang="scss" scoped>
.range-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.range-input {
flex: 1;
min-width: 0;
}
.range-sep {
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,158 @@
<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>
<ElSpace wrap>
<ElButton
v-permission="'dice:player_wallet_record:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:player_wallet_record:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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:player_wallet_record:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:player_wallet_record: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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/player_wallet_record/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
// 搜索表单
const searchForm = ref({
type: undefined,
username: undefined,
coin_min: undefined,
coin_max: undefined
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 类型展示0=充值 1=提现 2=购买抽奖次数
const typeFormatter = (row: Record<string, unknown>) =>
row.type === 0 ? '充值' : row.type === 1 ? '提现' : row.type === 2 ? '购买抽奖次数' : '-'
// 用户列展示为 dicePlayer.username兼容 dice_player
const usernameFormatter = (row: Record<string, any>) => {
const player = row.dicePlayer ?? row.dice_player
return player?.username ?? row.player_id ?? '-'
}
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => [
{ type: 'selection' },
{ prop: 'id', label: 'ID', width: 80 },
{ prop: 'player_id', label: '用户', width: 120, formatter: usernameFormatter },
{ prop: 'coin', label: '平台币变化', width: 110 },
{ prop: 'type', label: '类型', width: 120, formatter: typeFormatter },
{ prop: 'wallet_before', label: '钱包操作前', width: 110 },
{ prop: 'wallet_after', label: '钱包操作后', width: 110 },
{
prop: 'remark',
label: '备注',
width: 100,
showOverflowTooltip: true
},
{ prop: 'total_draw_count', label: '总抽奖次数' },
{ prop: 'paid_draw_count', label: '购买抽奖次数' },
{ prop: 'free_draw_count', label: '赠送抽奖次数' },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,229 @@
<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="player_id">
<el-select
v-model="formData.player_id"
placeholder="请选择用户(显示用户名)"
clearable
filterable
style="width: 100%"
:disabled="dialogType === 'edit'"
@change="onPlayerChange"
>
<el-option
v-for="item in playerOptions"
:key="item.id"
:label="item.username"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型" clearable style="width: 100%">
<el-option label="充值" :value="0" />
<el-option label="提现" :value="1" />
<el-option label="购买抽奖次数" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="平台币变化" prop="coin">
<el-input-number
v-model="formData.coin"
placeholder="正数增加、负数减少"
:precision="2"
style="width: 100%"
@change="onCoinChange"
/>
</el-form-item>
<el-form-item label="钱包操作前" prop="wallet_before">
<el-input-number
v-model="formData.wallet_before"
placeholder="选择用户后自动带出当前平台币"
:precision="2"
disabled
style="width: 100%"
/>
</el-form-item>
<el-form-item label="钱包操作后" prop="wallet_after">
<el-input-number
v-model="formData.wallet_after"
placeholder="根据平台币变化自动计算"
:precision="2"
disabled
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="2"
placeholder="选填"
maxlength="500"
show-word-limit
/>
</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/player_wallet_record/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>()
/** 玩家下拉选项id、username */
const playerOptions = ref<{ id: number; username: string }[]>([])
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const rules = reactive<FormRules>({
player_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
coin: [{ required: true, message: '平台币变化必填', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
})
const initialFormData = {
id: null as number | null,
player_id: null as number | null,
coin: 0 as number,
type: null as number | null,
wallet_before: 0 as number,
wallet_after: 0 as number,
remark: '' as string
}
const formData = reactive({ ...initialFormData })
/** 选择用户后拉取当前平台币作为钱包操作前 */
async function onPlayerChange(playerId: number | null) {
if (playerId == null) {
formData.wallet_before = 0
calcWalletAfter()
return
}
try {
const res = await api.getPlayerWalletBefore(playerId)
const before = res?.wallet_before ?? 0
formData.wallet_before = Number(before)
calcWalletAfter()
} catch {
formData.wallet_before = 0
calcWalletAfter()
}
}
/** 平台币变化时重算钱包操作后 */
function onCoinChange() {
calcWalletAfter()
}
/** 钱包操作后 = 钱包操作前 + 平台币变化 */
function calcWalletAfter() {
const before = Number(formData.wallet_before) || 0
const coin = Number(formData.coin) || 0
formData.wallet_after = Math.round((before + coin) * 100) / 100
}
watch(
() => props.modelValue,
async (open) => {
if (open) {
initPage()
try {
const list = await api.getPlayerOptions()
playerOptions.value = Array.isArray(list) ? list : []
} catch {
playerOptions.value = []
}
}
}
)
const initPage = async () => {
Object.assign(formData, initialFormData)
if (props.data) {
await nextTick()
initForm()
}
}
const numKeys = ['id', 'player_id', 'coin', 'type', 'wallet_before', 'wallet_after']
const initForm = () => {
if (!props.data) return
for (const key of Object.keys(formData)) {
if (!(key in props.data)) continue
const val = props.data[key]
if (numKeys.includes(key)) {
if (key === 'id' || key === 'player_id' || key === 'type') {
;(formData as any)[key] = val != null && val !== '' ? Number(val) : null
} else {
;(formData as any)[key] = val != null && val !== '' ? 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()
calcWalletAfter()
const payload = { ...formData }
if (props.dialogType === 'add') {
await api.save(payload)
ElMessage.success('新增成功')
} else {
await api.update(payload)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,116 @@
<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(5)">
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="全部" clearable style="width: 100%">
<el-option label="充值" :value="0" />
<el-option label="提现" :value="1" />
<el-option label="购买抽奖次数" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(5)">
<el-form-item label="用户(用户名)" prop="username">
<el-input v-model="formData.username" placeholder="按用户名搜索" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(8)">
<el-form-item label="平台币" prop="coin_min">
<div class="coin-range-wrap">
<el-input-number
v-model="formData.coin_min"
placeholder="最小"
:precision="2"
controls-position="right"
class="coin-range-input"
/>
<span class="coin-range-sep"></span>
<el-input-number
v-model="formData.coin_max"
placeholder="最大"
:precision="2"
controls-position="right"
class="coin-range-input"
/>
</div>
</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 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>
<style lang="scss" scoped>
.coin-range-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0; /* 允许在栅格内收缩,避免挤压右侧按钮 */
}
.coin-range-input {
flex: 1;
min-width: 0; /* 数字框可收缩 */
}
.coin-range-sep {
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,140 @@
<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>
<ElSpace wrap>
<ElButton v-permission="'dice:reward_config:index:save'" @click="showDialog('add')" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:reward_config:index:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
</ElSpace>
</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:reward_config:index:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'dice:reward_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"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/reward_config/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
// 搜索表单
const searchForm = ref<Record<string, unknown>>({
grid_number_min: undefined,
grid_number_max: undefined,
ui_text: undefined,
real_ev_min: undefined,
real_ev_max: undefined,
tier: undefined
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 表格配置(默认 100 条/页)
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
apiParams: { limit: 100 },
columnsFactory: () => [
{ type: 'selection' },
{ prop: 'id', label: 'ID(索引)', width: 80 },
{ prop: 'grid_number', label: '色子点数' },
{ prop: 'ui_text', label: '前端显示文本' },
{ prop: 'real_ev', label: '真实资金结算' },
{ prop: 'tier', label: '所属档位' },
{ prop: 'create_time', label: '创建时间' },
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
</script>

View File

@@ -0,0 +1,179 @@
<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="grid_number">
<el-input-number v-model="formData.grid_number" placeholder="请输入色子点数" />
</el-form-item>
<el-form-item label="前端显示文本" prop="ui_text">
<el-input v-model="formData.ui_text" placeholder="请输入前端显示文本" />
</el-form-item>
<el-form-item label="真实资金结算" prop="real_ev">
<el-input-number v-model="formData.real_ev" placeholder="请输入真实资金结算" />
</el-form-item>
<el-form-item label="所属档位" prop="tier">
<el-select
v-model="formData.tier"
placeholder="请选择所属档位"
clearable
style="width: 100%"
>
<el-option label="T1" value="T1" />
<el-option label="T2" value="T2" />
<el-option label="T3" value="T3" />
<el-option label="T4" value="T4" />
<el-option label="T5" value="T5" />
</el-select>
</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>
<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/reward_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 rules = reactive<FormRules>({
grid_number: [{ required: true, message: '色子点数必需填写', trigger: 'blur' }],
ui_text: [{ required: true, message: '前端显示文本必需填写', trigger: 'blur' }],
real_ev: [{ required: true, message: '真实资金结算必需填写', trigger: 'blur' }],
tier: [{ required: true, message: '所属档位必需填写', trigger: 'blur' }]
})
/**
* 初始数据
*/
const initialFormData = {
id: null,
grid_number: null,
ui_text: '',
real_ev: '',
tier: '',
remark: ''
}
/**
* 表单数据
*/
const formData = reactive({ ...initialFormData })
/**
* 监听弹窗打开,初始化表单数据
*/
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
initPage()
}
}
)
/**
* 初始化页面数据
*/
const initPage = async () => {
// 先重置为初始值
Object.assign(formData, initialFormData)
// 如果有数据,则填充数据
if (props.data) {
await nextTick()
initForm()
}
}
/**
* 初始化表单数据
*/
const initForm = () => {
if (props.data) {
for (const key in formData) {
if (props.data[key] != null && props.data[key] != undefined) {
;(formData as any)[key] = props.data[key]
}
}
}
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
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,137 @@
<template>
<sa-search-bar
ref="searchBarRef"
v-model="formData"
label-width="120px"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
@expand="handleExpand"
>
<el-col v-bind="setSpan(6)">
<el-form-item label="色子点数" prop="grid_number_min">
<div class="range-wrap">
<el-input-number
v-model="formData.grid_number_min"
placeholder="最小"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.grid_number_max"
placeholder="最大"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="前端显示文本" prop="ui_text">
<el-input v-model="formData.ui_text" placeholder="模糊查询" clearable />
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="真实资金结算" prop="real_ev_min">
<div class="range-wrap">
<el-input-number
v-model="formData.real_ev_min"
placeholder="最小"
:precision="2"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.real_ev_max"
placeholder="最大"
:precision="2"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="所属档位" prop="tier">
<el-select v-model="formData.tier" placeholder="全部" clearable style="width: 100%">
<el-option label="T1" value="T1" />
<el-option label="T2" value="T2" />
<el-option label="T3" value="T3" />
<el-option label="T4" value="T4" />
<el-option label="T5" value="T5" />
</el-select>
</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 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,
md: span >= 8 ? span : 8,
lg: span,
xl: span
}
}
</script>
<style lang="scss" scoped>
.range-wrap {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.range-input {
flex: 1;
min-width: 0;
}
.range-sep {
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,122 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\lottery_config;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\lottery_config\DiceLotteryConfigLogic;
use app\dice\validate\lottery_config\DiceLotteryConfigValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 色子奖池配置控制器
*/
class DiceLotteryConfigController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DiceLotteryConfigLogic();
$this->validate = new DiceLotteryConfigValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('色子奖池配置列表', 'dice:lottery_config:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['name', ''],
['type', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('色子奖池配置读取', 'dice:lottery_config:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('色子奖池配置添加', 'dice:lottery_config:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('色子奖池配置修改', 'dice:lottery_config:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('色子奖池配置删除', 'dice:lottery_config:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,179 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\play_record;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\play_record\DicePlayRecordLogic;
use app\dice\validate\play_record\DicePlayRecordValidate;
use app\dice\model\player\DicePlayer;
use app\dice\model\lottery_config\DiceLotteryConfig;
use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 玩家抽奖记录控制器
*/
class DicePlayRecordController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DicePlayRecordLogic();
$this->validate = new DicePlayRecordValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['username', ''],
['lottery_config_name', ''],
['lottery_type', ''],
['is_win', ''],
['win_coin_min', ''],
['win_coin_max', ''],
['reward_ui_text', ''],
['reward_tier', ''],
]);
$query = $this->logic->search($where);
$query->with([
'dicePlayer',
'diceRewardConfig',
'diceLotteryConfig',
]);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 获取玩家选项id、username
*/
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
return $this->success($data);
}
/**
* 获取彩金池配置选项id、name
*/
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getLotteryConfigOptions(Request $request): Response
{
$list = DiceLotteryConfig::field('id,name')->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'name' => $item['name'] ?? ''];
})->toArray();
return $this->success($data);
}
/**
* 获取奖励配置选项id、ui_text、tier
*/
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getRewardConfigOptions(Request $request): Response
{
$list = DiceRewardConfig::field('id,ui_text,tier')->select();
$data = $list->map(function ($item) {
return [
'id' => $item['id'],
'ui_text' => $item['ui_text'] ?? '',
'tier' => $item['tier'] ?? ''
];
})->toArray();
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('玩家抽奖记录读取', 'dice:play_record:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('玩家抽奖记录添加', 'dice:play_record:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('玩家抽奖记录修改', 'dice:play_record:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('玩家抽奖记录删除', 'dice:play_record:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\player;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player\DicePlayerLogic;
use app\dice\validate\player\DicePlayerValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 大富翁-玩家控制器
*/
class DicePlayerController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DicePlayerLogic();
$this->validate = new DicePlayerValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家列表', 'dice:player:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['username', ''],
['name', ''],
['status', ''],
['coin', ''],
['is_up', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家读取', 'dice:player:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家添加', 'dice:player:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家修改', 'dice:player:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 仅更新状态(列表内开关用)
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家修改', 'dice:player:index:update')]
public function updateStatus(Request $request): Response
{
$id = $request->input('id');
$status = $request->input('status');
if ($id === null || $id === '') {
return $this->fail('缺少 id');
}
if ($status === null || $status === '') {
return $this->fail('缺少 status');
}
$this->logic->edit($id, ['status' => (int) $status]);
return $this->success('修改成功');
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('大富翁-玩家删除', 'dice:player:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\player_coin_record;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player_coin_record\DicePlayerCoinRecordLogic;
use app\dice\validate\player_coin_record\DicePlayerCoinRecordValidate;
use app\dice\model\player\DicePlayer;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 玩家购买抽奖记录控制器
*/
class DicePlayerCoinRecordController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DicePlayerCoinRecordLogic();
$this->validate = new DicePlayerCoinRecordValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录列表', 'dice:player_coin_record:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['username', ''],
['use_coins_min', ''],
['use_coins_max', ''],
['total_draw_count_min', ''],
['total_draw_count_max', ''],
['paid_draw_count_min', ''],
['paid_draw_count_max', ''],
['free_draw_count_min', ''],
['free_draw_count_max', ''],
['create_time_min', ''],
['create_time_max', ''],
]);
$query = $this->logic->search($where);
$query->with([
'dicePlayer',
]);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 获取玩家选项id、username用于下拉
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录列表', 'dice:player_coin_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录读取', 'dice:player_coin_record:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录添加', 'dice:player_coin_record:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录修改', 'dice:player_coin_record:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('玩家购买抽奖记录删除', 'dice:player_coin_record:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,163 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\player_wallet_record;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player_wallet_record\DicePlayerWalletRecordLogic;
use app\dice\validate\player_wallet_record\DicePlayerWalletRecordValidate;
use app\dice\model\player\DicePlayer;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 玩家钱包流水控制器
*/
class DicePlayerWalletRecordController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DicePlayerWalletRecordLogic();
$this->validate = new DicePlayerWalletRecordValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水列表', 'dice:player_wallet_record:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['type', ''],
['username', ''],
['coin_min', ''],
['coin_max', ''],
]);
$query = $this->logic->search($where);
$query->with([
'dicePlayer',
]);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 获取玩家选项id、username用于下拉
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水列表', 'dice:player_wallet_record:index:index')]
public function getPlayerOptions(Request $request): Response
{
$list = DicePlayer::field('id,username')->select();
$data = $list->map(function ($item) {
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
})->toArray();
return $this->success($data);
}
/**
* 获取指定玩家当前平台币(用于钱包操作前)
* 类型 0=充值 1=提现 2=购买抽奖次数,操作前均为当前平台币
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水读取', 'dice:player_wallet_record:index:read')]
public function getPlayerWalletBefore(Request $request): Response
{
$playerId = $request->input('player_id');
if ($playerId === null || $playerId === '') {
return $this->fail('缺少 player_id');
}
$player = DicePlayer::field('coin')->where('id', $playerId)->find();
if (!$player) {
return $this->fail('玩家不存在');
}
return $this->success(['wallet_before' => (float) $player['coin']]);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水读取', 'dice:player_wallet_record:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水添加', 'dice:player_wallet_record:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水修改', 'dice:player_wallet_record:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('玩家钱包流水删除', 'dice:player_wallet_record:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,126 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\reward_config;
use plugin\saiadmin\basic\BaseController;
use app\dice\logic\reward_config\DiceRewardConfigLogic;
use app\dice\validate\reward_config\DiceRewardConfigValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 奖励配置控制器
*/
class DiceRewardConfigController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DiceRewardConfigLogic();
$this->validate = new DiceRewardConfigValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
['grid_number_min', ''],
['grid_number_max', ''],
['ui_text', ''],
['real_ev_min', ''],
['real_ev_max', ''],
['tier', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置读取', 'dice:reward_config:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置添加', 'dice:reward_config:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置修改', 'dice:reward_config:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置删除', 'dice:reward_config:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\lottery_config;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\lottery_config\DiceLotteryConfig;
/**
* 色子奖池配置逻辑层
*/
class DiceLotteryConfigLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DiceLotteryConfig();
}
}

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\play_record;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\play_record\DicePlayRecord;
/**
* 玩家抽奖记录逻辑层
*/
class DicePlayRecordLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DicePlayRecord();
}
}

View File

@@ -0,0 +1,61 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\player;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\player\DicePlayer;
/**
* 大富翁-玩家逻辑层
*/
class DicePlayerLogic extends BaseLogic
{
/** 密码加密盐(可与 config 统一) */
private const PASSWORD_SALT = 'dice_player_salt_2024';
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DicePlayer();
}
/**
* 添加数据(密码 md5+salt 加密)
*/
public function add(array $data): mixed
{
if (!empty($data['password'])) {
$data['password'] = $this->hashPassword($data['password']);
}
return parent::add($data);
}
/**
* 修改数据(仅当传入 password 时用 md5+salt 加密后更新)
*/
public function edit($id, array $data): mixed
{
if (isset($data['password']) && $data['password'] !== '') {
$data['password'] = $this->hashPassword($data['password']);
} else {
unset($data['password']);
}
return parent::edit($id, $data);
}
/**
* 密码加密md5(salt . password)
*/
private function hashPassword(string $password): string
{
return md5(self::PASSWORD_SALT . $password);
}
}

View File

@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\player_coin_record;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\player_coin_record\DicePlayerCoinRecord;
/**
* 玩家购买抽奖记录逻辑层
*/
class DicePlayerCoinRecordLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DicePlayerCoinRecord();
}
/**
* 添加前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
*/
public function add(array $data): mixed
{
$data = $this->fillTotalDrawCount($data);
return parent::add($data);
}
/**
* 修改前:总抽奖次数 = 购买抽奖次数 + 赠送抽奖次数
*/
public function edit($id, array $data): mixed
{
$data = $this->fillTotalDrawCount($data);
return parent::edit($id, $data);
}
private function fillTotalDrawCount(array $data): array
{
$paid = isset($data['paid_draw_count']) ? (int) $data['paid_draw_count'] : 0;
$free = isset($data['free_draw_count']) ? (int) $data['free_draw_count'] : 0;
$data['total_draw_count'] = $paid + $free;
return $data;
}
}

View File

@@ -0,0 +1,38 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\player_wallet_record;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
/**
* 玩家钱包流水逻辑层
*/
class DicePlayerWalletRecordLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DicePlayerWalletRecord();
}
/**
* 添加数据(补全抽奖次数字段默认值)
*/
public function add(array $data): mixed
{
$data['total_draw_count'] = $data['total_draw_count'] ?? 0;
$data['paid_draw_count'] = $data['paid_draw_count'] ?? 0;
$data['free_draw_count'] = $data['free_draw_count'] ?? 0;
return parent::add($data);
}
}

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\reward_config;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\reward_config\DiceRewardConfig;
/**
* 奖励配置逻辑层
*/
class DiceRewardConfigLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DiceRewardConfig();
}
}

View File

@@ -0,0 +1,51 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\lottery_config;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 色子奖池配置模型
*
* dice_lottery_config 色子奖池配置
*
* @property $id ID
* @property $name 名称
* @property $remark 备注
* @property $type 奖池类型
* @property $safety_line 安全线
* @property $create_time 创建时间
* @property $update_time 修改时间
* @property $t1_wight T1池权重
* @property $t2_wight T2池权重
* @property $t3_wight T3池权重
* @property $t4_wight T4池权重
* @property $t5_wight T5池权重
*/
class DiceLotteryConfig extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_lottery_config';
/**
* 名称 搜索
*/
public function searchNameAttr($query, $value)
{
$query->where('name', 'like', '%'.$value.'%');
}
}

View File

@@ -0,0 +1,160 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\play_record;
use app\dice\model\lottery_config\DiceLotteryConfig;
use app\dice\model\player\DicePlayer;
use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\basic\think\BaseModel;
use think\model\relation\BelongsTo;
/**
* 玩家抽奖记录模型
*
* dice_play_record 玩家抽奖记录
*
* @property $id ID
* @property $player_id 玩家id
* @property $lottery_config_id 彩金池配置
* @property $lottery_type 抽奖类型
* @property $is_win 中奖
* @property $win_coin 赢取平台币
* @property $reward_config_id 奖励配置id
* @property $lottery_id 奖池
* @property $lottery_name 奖池名
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class DicePlayRecord extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_play_record';
/**
* 玩家
* 关联模型 dicePlayer
*/
public function dicePlayer(): BelongsTo
{
return $this->belongsTo(DicePlayer::class, 'player_id', 'id');
}
/**
* 中奖配置
* 关联模型 diceRewardConfig
*/
public function diceRewardConfig(): BelongsTo
{
return $this->belongsTo(DiceRewardConfig::class, 'reward_config_id', 'id');
}
/**
* 彩金配置
* 关联模型 diceLotteryConfig
*/
public function diceLotteryConfig(): BelongsTo
{
return $this->belongsTo(DiceLotteryConfig::class, 'lottery_config_id', 'id');
}
/** 按玩家用户名模糊dicePlayer.username */
public function searchUsernameAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$ids = DicePlayer::where('username', 'like', '%' . $value . '%')->column('id');
if (!empty($ids)) {
$query->whereIn('player_id', $ids);
} else {
$query->whereRaw('1=0');
}
}
/** 按彩金池配置名称模糊diceLotteryConfig.name */
public function searchLotteryConfigNameAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$ids = DiceLotteryConfig::where('name', 'like', '%' . $value . '%')->column('id');
if (!empty($ids)) {
$query->whereIn('lottery_config_id', $ids);
} else {
$query->whereRaw('1=0');
}
}
/** 抽奖类型 */
public function searchLotteryTypeAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('lottery_type', '=', $value);
}
}
/** 中奖 */
public function searchIsWinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('is_win', '=', $value);
}
}
/** 赢取平台币下限 */
public function searchWinCoinMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('win_coin', '>=', $value);
}
}
/** 赢取平台币上限 */
public function searchWinCoinMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('win_coin', '<=', $value);
}
}
/** 按奖励配置前端显示文本模糊diceRewardConfig.ui_text */
public function searchRewardUiTextAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$ids = DiceRewardConfig::where('ui_text', 'like', '%' . $value . '%')->column('id');
if (!empty($ids)) {
$query->whereIn('reward_config_id', $ids);
} else {
$query->whereRaw('1=0');
}
}
/** 按奖励档位diceRewardConfig.tier中奖名 T1-T5 */
public function searchRewardTierAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$ids = DiceRewardConfig::where('tier', '=', $value)->column('id');
if (!empty($ids)) {
$query->whereIn('reward_config_id', $ids);
} else {
$query->whereRaw('1=0');
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\player;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 大富翁-玩家模型
*
* dice_player 大富翁-玩家
*
* @property $id ID
* @property $username 用户名
* @property $name 昵称
* @property $password 密码
* @property $status 状态
* @property $coin 平台币
* @property $is_up 倍率
* @property $t1_wight T1池权重
* @property $t2_wight T2池权重
* @property $t3_wight T3池权重
* @property $t4_wight T4池权重
* @property $t5_wight T5池权重
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $created_at 创建时间
* @property $updated_at 更新时间
* @property $deleted_at 删除时间
*/
class DicePlayer extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_player';
/**
* 用户名 搜索
*/
public function searchUsernameAttr($query, $value)
{
$query->where('username', 'like', '%'.$value.'%');
}
/**
* 昵称 搜索
*/
public function searchNameAttr($query, $value)
{
$query->where('name', 'like', '%'.$value.'%');
}
/**
* 状态 搜索
*/
public function searchStatusAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('status', '=', $value);
}
}
/**
* 平台币 搜索
*/
public function searchCoinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('coin', '=', $value);
}
}
/**
* 倍率 搜索
*/
public function searchIs_upAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('is_up', '=', $value);
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\player_coin_record;
use app\dice\model\player\DicePlayer;
use plugin\saiadmin\basic\think\BaseModel;
use think\model\relation\BelongsTo;
/**
* 玩家购买抽奖记录模型
*
* dice_player_coin_record 玩家购买抽奖记录
*
* @property $id ID
* @property $player_id 玩家id
* @property $use_coins 消耗硬币
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $remark 备注
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class DicePlayerCoinRecord extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_player_coin_record';
/**
* 关联模型 dicePlayer
*/
public function dicePlayer(): BelongsTo
{
return $this->belongsTo(DicePlayer::class, 'player_id', 'id');
}
/**
* 按关联玩家用户名搜索dicePlayer.username
*/
public function searchUsernameAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$playerIds = DicePlayer::where('username', 'like', '%' . $value . '%')->column('id');
if (!empty($playerIds)) {
$query->whereIn('player_id', $playerIds);
} else {
$query->whereRaw('1=0');
}
}
/** 消耗硬币下限 */
public function searchUseCoinsMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('use_coins', '>=', $value);
}
}
/** 消耗硬币上限 */
public function searchUseCoinsMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('use_coins', '<=', $value);
}
}
/** 总抽奖次数下限 */
public function searchTotalDrawCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('total_draw_count', '>=', $value);
}
}
/** 总抽奖次数上限 */
public function searchTotalDrawCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('total_draw_count', '<=', $value);
}
}
/** 购买抽奖次数下限 */
public function searchPaidDrawCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('paid_draw_count', '>=', $value);
}
}
/** 购买抽奖次数上限 */
public function searchPaidDrawCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('paid_draw_count', '<=', $value);
}
}
/** 赠送抽奖次数下限 */
public function searchFreeDrawCountMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('free_draw_count', '>=', $value);
}
}
/** 赠送抽奖次数上限 */
public function searchFreeDrawCountMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('free_draw_count', '<=', $value);
}
}
/** 创建时间起始 */
public function searchCreateTimeMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('create_time', '>=', $value);
}
}
/** 创建时间结束 */
public function searchCreateTimeMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('create_time', '<=', $value);
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\player_wallet_record;
use app\dice\model\player\DicePlayer;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 玩家钱包流水模型
*
* dice_player_wallet_record 玩家钱包流水记录
*
* @property $id ID
* @property $player_id 用户id
* @property $coin 平台币变化
* @property $type 类型:0=充值 1=提现 2=购买抽奖次数
* @property $wallet_before 钱包操作前
* @property $wallet_after 钱包操作后
* @property $total_draw_count 总抽奖次数
* @property $paid_draw_count 购买抽奖次数
* @property $free_draw_count 赠送抽奖次数
* @property $remark 备注
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class DicePlayerWalletRecord extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_player_wallet_record';
/**
* 关联模型 dicePlayer
*/
public function dicePlayer(): \think\model\relation\BelongsTo
{
return $this->belongsTo(DicePlayer::class, 'player_id', 'id');
}
/**
* 类型 搜索
*/
public function searchTypeAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('type', '=', $value);
}
}
/**
* 按关联玩家用户名搜索dicePlayer.username
*/
public function searchUsernameAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$playerIds = DicePlayer::where('username', 'like', '%' . $value . '%')->column('id');
if (!empty($playerIds)) {
$query->whereIn('player_id', $playerIds);
} else {
$query->whereRaw('1=0');
}
}
/**
* 平台币下限 搜索withSearch 用 Str::studly 故方法名为 searchCoinMinAttr
*/
public function searchCoinMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('coin', '>=', $value);
}
}
/**
* 平台币上限 搜索withSearch 用 Str::studly 故方法名为 searchCoinMaxAttr
*/
public function searchCoinMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('coin', '<=', $value);
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\reward_config;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 奖励配置模型
*
* dice_reward_config 奖励配置
*
* @property $id ID
* @property $grid_number 色子点数
* @property $ui_text 前端显示文本
* @property $real_ev 真实资金结算
* @property $tier 所属档位
* @property $remark 备注
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class DiceRewardConfig extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据库表名称
* @var string
*/
protected $table = 'dice_reward_config';
/** 色子点数下限 */
public function searchGridNumberMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('grid_number', '>=', $value);
}
}
/** 色子点数上限 */
public function searchGridNumberMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('grid_number', '<=', $value);
}
}
/** 前端显示文本模糊 */
public function searchUiTextAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('ui_text', 'like', '%' . $value . '%');
}
}
/** 真实资金结算下限 */
public function searchRealEvMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('real_ev', '>=', $value);
}
}
/** 真实资金结算上限 */
public function searchRealEvMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('real_ev', '<=', $value);
}
}
/** 所属档位 */
public function searchTierAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('tier', '=', $value);
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\lottery_config;
use plugin\saiadmin\basic\BaseValidate;
/**
* 色子奖池配置验证器
*/
class DiceLotteryConfigValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'type' => 'require',
't1_wight' => 'require',
't2_wight' => 'require',
't3_wight' => 'require',
't4_wight' => 'require',
't5_wight' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '名称必须填写',
'type' => '奖池类型必须填写',
't1_wight' => 'T1池权重必须填写',
't2_wight' => 'T2池权重必须填写',
't3_wight' => 'T3池权重必须填写',
't4_wight' => 'T4池权重必须填写',
't5_wight' => 'T5池权重必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'type',
't1_wight',
't2_wight',
't3_wight',
't4_wight',
't5_wight',
],
'update' => [
'name',
'type',
't1_wight',
't2_wight',
't3_wight',
't4_wight',
't5_wight',
],
];
}

View File

@@ -0,0 +1,62 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\play_record;
use plugin\saiadmin\basic\BaseValidate;
/**
* 玩家抽奖记录验证器
*/
class DicePlayRecordValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'player_id' => 'require',
'lottery_config_id' => 'require',
'lottery_type' => 'require',
'is_win' => 'require',
'win_coin' => 'require',
'reward_config_id' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'player_id' => '玩家必须填写',
'lottery_config_id' => '彩金池配置必须填写',
'lottery_type' => '抽奖类型必须填写',
'is_win' => '中奖必须填写',
'win_coin' => '赢取平台币必须填写',
'reward_config_id' => '奖励配置必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'player_id',
'lottery_config_id',
'lottery_type',
'is_win',
'win_coin',
'reward_config_id',
],
'update' => [
'player_id',
'lottery_config_id',
'lottery_type',
'is_win',
'win_coin',
'reward_config_id',
],
];
}

View File

@@ -0,0 +1,57 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\player;
use plugin\saiadmin\basic\BaseValidate;
/**
* 大富翁-玩家验证器
*/
class DicePlayerValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'username' => 'require',
'name' => 'require',
'password' => 'require',
'status' => 'require',
'coin' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'username' => '用户名必须填写',
'name' => '昵称必须填写',
'password' => '密码必须填写',
'status' => '状态必须填写',
'coin' => '平台币必须填写',
];
/**
* 定义场景update 时密码可选,不填则不修改)
*/
protected $scene = [
'save' => [
'username',
'name',
'password',
'status',
'coin',
],
'update' => [
'username',
'name',
'status',
'coin',
],
];
}

View File

@@ -0,0 +1,62 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\player_coin_record;
use plugin\saiadmin\basic\BaseValidate;
/**
* 玩家购买抽奖记录验证器
*/
class DicePlayerCoinRecordValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'player_id' => 'require',
'use_coins' => 'require',
'total_draw_count' => 'require',
'paid_draw_count' => 'require',
'free_draw_count' => 'require',
'remark' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'player_id' => '玩家id必须填写',
'use_coins' => '消耗硬币必须填写',
'total_draw_count' => '总抽奖次数必须填写',
'paid_draw_count' => '购买抽奖次数必须填写',
'free_draw_count' => '赠送抽奖次数必须填写',
'remark' => '备注必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'remark',
],
'update' => [
'player_id',
'use_coins',
'total_draw_count',
'paid_draw_count',
'free_draw_count',
'remark',
],
];
}

View File

@@ -0,0 +1,46 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\player_wallet_record;
use plugin\saiadmin\basic\BaseValidate;
/**
* 玩家钱包流水验证器
*/
class DicePlayerWalletRecordValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'player_id' => 'require',
'coin' => 'require',
'type' => 'require',
'wallet_before' => 'require',
'wallet_after' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'player_id' => '用户必须选择',
'coin' => '平台币变化必须填写',
'type' => '类型必须填写',
'wallet_before' => '钱包操作前不能为空',
'wallet_after' => '钱包操作后不能为空',
];
/**
* 定义场景
*/
protected $scene = [
'save' => ['player_id', 'coin', 'type', 'wallet_before', 'wallet_after'],
'update' => ['player_id', 'coin', 'type', 'wallet_before', 'wallet_after'],
];
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\reward_config;
use plugin\saiadmin\basic\BaseValidate;
/**
* 奖励配置验证器
*/
class DiceRewardConfigValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'grid_number' => 'require',
'ui_text' => 'require',
'real_ev' => 'require',
'tier' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'grid_number' => '色子点数必须填写',
'ui_text' => '前端显示文本必须填写',
'real_ev' => '真实资金结算必须填写',
'tier' => '所属档位必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'grid_number',
'ui_text',
'real_ev',
'tier',
],
'update' => [
'grid_number',
'ui_text',
'real_ev',
'tier',
],
];
}

View File

@@ -2,3 +2,15 @@
/** /**
* Here is your custom functions. * Here is your custom functions.
*/ */
/**
* mb_split 兼容:当 PHP 未启用 mbstring 或 mb_split 不可用时,用 preg_split 模拟
* 解决 Illuminate\Support\Str::studly() 在 Windows 等环境下报 Call to undefined function mb_split() 的问题
*/
if (!function_exists('mb_split')) {
function mb_split(string $pattern, string $string, int $limit = -1): array
{
$regex = '/' . str_replace('/', '\\/', $pattern) . '/u';
return preg_split($regex, $string, $limit === -1 ? -1 : $limit) ?: [];
}
}