麻将胡了
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 箱子列表(分页;boxType 只有一个 normal,留参数只为兼容后台筛选框):MahjongAdminController.boxList
|
||||
// 返回 { rows:[{...各身份 targetRtpByAudience/actualRtpByAudience/totalRoundsByAudience}], total }
|
||||
export function getBoxList(params) {
|
||||
return request({ url: '/admin/mahjong/box/list', method: 'get', params })
|
||||
}
|
||||
|
||||
// 新增/编辑箱子:MahjongAdminController.boxSave(price = bet,本游戏无投注倍数、costMultiplier ≡ 1)
|
||||
export function saveBox(data) {
|
||||
return request({ url: '/admin/mahjong/box/save', method: 'post', data })
|
||||
}
|
||||
|
||||
// 某箱某身份的倍率桶分布:MahjongAdminController.boxBuckets
|
||||
// 返回 { list:[{bucketId,count,used,remaining,convergeCount,...}], audience, fixedRounds, convergeRounds, poolExpect }
|
||||
export function getBoxBuckets(boxId, audience) {
|
||||
return request({ url: `/admin/mahjong/box/buckets/${boxId}`, method: 'get', params: { audience }})
|
||||
}
|
||||
|
||||
// 保存某箱某身份倍率桶分布(items 为 [{bucketId,count}]):MahjongAdminController.boxBucketsSave
|
||||
export function saveBoxBuckets(data) {
|
||||
return request({ url: '/admin/mahjong/box/buckets/save', method: 'post', data })
|
||||
}
|
||||
|
||||
// 按目标 RTP 重算某箱某身份分布:MahjongAdminController.calcDistribution
|
||||
export function calcDistribution(data) {
|
||||
return request({ url: '/admin/mahjong/box/buckets/calc', method: 'post', data })
|
||||
}
|
||||
|
||||
// 模拟 payoutCap 约束下的预估实际 RTP:MahjongAdminController.simulateRtp
|
||||
export function simulateRtp(data) {
|
||||
return request({ url: '/admin/mahjong/box/buckets/simulate-rtp', method: 'post', data })
|
||||
}
|
||||
|
||||
// ==================== 分布模板(按身份) ====================
|
||||
|
||||
// 模板列表:MahjongAdminController.templateList
|
||||
export function getBoxTemplates(boxId, audience) {
|
||||
return request({ url: `/admin/mahjong/box/templates/${boxId}`, method: 'get', params: { audience }})
|
||||
}
|
||||
// 保存为模板:MahjongAdminController.templateSave
|
||||
export function saveBoxTemplate(data) {
|
||||
return request({ url: '/admin/mahjong/box/template/save', method: 'post', data })
|
||||
}
|
||||
// 删除模板:MahjongAdminController.templateDelete
|
||||
export function deleteBoxTemplate(id) {
|
||||
return request({ url: `/admin/mahjong/box/template/${id}`, method: 'delete' })
|
||||
}
|
||||
// 应用选中模板:MahjongAdminController.templateApply
|
||||
export function applyBoxTemplate(data) {
|
||||
return request({ url: '/admin/mahjong/box/template/apply', method: 'post', data })
|
||||
}
|
||||
|
||||
// 一键重算所有箱子 × 所有身份(按各箱现有配置):MahjongAdminController.recalcAll
|
||||
export function recalcAll() {
|
||||
return request({ url: '/admin/mahjong/box/recalc_all', method: 'post' })
|
||||
}
|
||||
|
||||
// 重算分布:按参数批量重算(boxType 空=全部;audience 空=全部身份):MahjongAdminController.recalcBatch
|
||||
export function recalcBatch(data) {
|
||||
return request({ url: '/admin/mahjong/box/recalcBatch', method: 'post', data })
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 倍率桶定义列表(16 档 0x~500x):MahjongAdminController.bucketList
|
||||
export function getBucketList() {
|
||||
return request({ url: '/admin/mahjong/bucket/list', method: 'get' })
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 每日统计列表(分页 + 按日期区间):MahjongAdminController.winList
|
||||
export function getWinList(params) {
|
||||
return request({ url: '/admin/mahjong/win/list', method: 'get', params })
|
||||
}
|
||||
|
||||
// 汇总(按日期区间):MahjongAdminController.winSummary
|
||||
export function getWinSummary(params) {
|
||||
return request({ url: '/admin/mahjong/win/summary', method: 'get', params })
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 返奖饰品列表(含返奖区间 winLow/winHigh、品质图 levelImg):MahjongAdminController.prizeList
|
||||
export function prizeList() {
|
||||
return request({ url: '/admin/mahjong/prize/list', method: 'get' })
|
||||
}
|
||||
|
||||
// 新增/编辑返奖饰品(同步写 game_mahjong_prize_range):MahjongAdminController.prizeSave
|
||||
export function prizeSave(data) {
|
||||
return request({ url: '/admin/mahjong/prize/save', method: 'post', data })
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 玩家记录列表(分页 + 按玩家ID/身份筛选;audience=common/anchor/white,空=全部身份)
|
||||
// MahjongAdminController.recordList(行内回填 audience)
|
||||
export function getRecordList(params) {
|
||||
return request({ url: '/admin/mahjong/record/list', method: 'get', params })
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 奖池配置快照(总池 + 各箱池),按身份 audience=common/anchor/white
|
||||
// MahjongAdminController.rewardPoolConfig
|
||||
export function getPoolConfig(params) {
|
||||
return request({ url: '/admin/mahjong/rewardPool/config', method: 'get', params })
|
||||
}
|
||||
|
||||
// 逐箱注水/抽水(type=1 注水 / 2 抽水,含 audience、remark):MahjongAdminController.rewardPoolAdjust
|
||||
export function adjustPool(data) {
|
||||
return request({ url: '/admin/mahjong/rewardPool/adjust', method: 'post', data })
|
||||
}
|
||||
|
||||
// 批量按一轮花费比例注水/抽水:{ audience, type, ratio, remark }
|
||||
// 本游戏无投注倍数,每箱基数 = bet × 总回合数;MahjongAdminController.rewardPoolAdjustBatch
|
||||
export function adjustPoolBatch(data) {
|
||||
return request({ url: '/admin/mahjong/rewardPool/adjustBatch', method: 'post', data })
|
||||
}
|
||||
|
||||
// 重置人工调整:该身份所有箱子注水/抽水累计归零,只保留真实水位(下注−返奖):{ audience, remark }
|
||||
// MahjongAdminController.rewardPoolAdjustReset
|
||||
export function adjustPoolReset(data) {
|
||||
return request({ url: '/admin/mahjong/rewardPool/adjustReset', method: 'post', data })
|
||||
}
|
||||
|
||||
// 奖池调整历史(分页,params 含 audience):MahjongAdminController.rewardPoolAdjustLog
|
||||
export function getPoolLogs(params) {
|
||||
return request({ url: '/admin/mahjong/rewardPool/adjustLog', method: 'get', params })
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 预生成回合列表(分页 + 按回合ID/箱子筛选):MahjongAdminController.roundList
|
||||
export function getRoundList(params) {
|
||||
return request({ url: '/admin/mahjong/round/list', method: 'get', params })
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="10" style="margin-bottom:16px">
|
||||
<el-input v-model="queryParams.name" placeholder="按赌注搜索" clearable style="width:200px;margin-right:10px" @clear="loadData" @keyup.enter.native="loadData">
|
||||
<el-button slot="append" icon="el-icon-search" @click="loadData" />
|
||||
</el-input>
|
||||
<span style="margin-left:10px;margin-right:4px;color:#606266">身份:</span>
|
||||
<el-radio-group v-model="audienceFilter" size="small" @change="loadData">
|
||||
<el-radio-button v-for="a in audiences" :key="a.key" :label="a.key">{{ a.name }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button icon="el-icon-refresh" style="margin-left:10px" @click="loadData">刷新</el-button>
|
||||
<el-button type="danger" icon="el-icon-refresh" :loading="recalcLoading" style="float:right" @click="openRecalcDialog">一键重算分布</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="boxList" border>
|
||||
<el-table-column prop="boxId" label="ID" min-width="50" align="center" />
|
||||
<el-table-column prop="bet" label="赌注" min-width="70" align="center" />
|
||||
<el-table-column label="总回合" min-width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="totalRoundsOf(scope.row) != null">{{ totalRoundsOf(scope.row) }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标RTP" min-width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<!-- 显示 1 位小数:配比是"整数条回合 × 桶内实际均值"分出来的,精确值留在 title 里,鼠标悬停可看 -->
|
||||
<span v-if="targetRtp(scope.row) != null" :title="targetRtp(scope.row) + '%'">{{ Number(targetRtp(scope.row)).toFixed(1) + '%' }}</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实时RTP" min-width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="actualRtp(scope.row) != null" :style="{color: actualRtp(scope.row) > (targetRtp(scope.row) != null ? targetRtp(scope.row) : 0) ? '#f56c6c' : '#67c23a'}">{{ actualRtp(scope.row) }}%</span>
|
||||
<span v-else style="color:#c0c4cc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="60" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">{{ scope.row.status === '1' ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="270" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="info" @click="handleBuckets(scope.row)">饰品配置</el-button>
|
||||
<el-button size="mini" type="success" @click="openTemplateManager(scope.row)">模板管理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top:16px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="total"
|
||||
:page-size.sync="queryParams.pageSize"
|
||||
:current-page.sync="queryParams.pageNum"
|
||||
:page-sizes="[10,20,50,100]"
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<el-dialog
|
||||
:title="'编辑箱子 - 赌注 ' + (form.bet != null ? form.bet : '')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="form" :model="form" label-width="110px">
|
||||
<el-form-item label="基本赌注"><el-input-number v-model="form.bet" disabled style="width:100%" /></el-form-item>
|
||||
<el-form-item label="总回合数">
|
||||
<el-input-number v-model="form.totalRounds" :min="0" :step="50" style="width:100%" />
|
||||
<span style="color:#909399;font-size:12px">身份级参数,仅作用于身份【{{ audienceLabel(audienceFilter) }}】</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标RTP">
|
||||
<el-input-number v-model="form.rtp" :min="0" :max="1" :precision="3" :step="0.05" style="width:100%" />
|
||||
<span style="color:#909399;font-size:12px">身份级参数,仅作用于身份【{{ audienceLabel(audienceFilter) }}】</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="固定/收敛回合%">
|
||||
<el-input-number v-model="form.fixedPct" :min="0" :max="100" :step="5" style="width:38%" @change="onFixedPctChange" />
|
||||
<span style="margin-left:4px">%</span>
|
||||
<span style="margin:0 4%">/</span>
|
||||
<el-input-number v-model="form.convergePct" :min="0" :max="100" :step="5" style="width:38%" @change="onConvergePctChange" />
|
||||
<span style="margin-left:4px">%</span>
|
||||
<div style="color:#909399;font-size:12px">固定% + 收敛% = 100;收敛回合用于一轮内把实时RTP动态拉回目标,随保存作用于当前身份</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="1">启用</el-radio>
|
||||
<el-radio label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="saveLoading" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重算分布弹窗 -->
|
||||
<el-dialog
|
||||
title="重算分布"
|
||||
:visible.sync="recalcDialogVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="身份范围">
|
||||
<el-radio-group v-model="recalcForm.onlyCurrent">
|
||||
<el-radio :label="false">全部身份</el-radio>
|
||||
<el-radio :label="true">仅当前身份【{{ audienceLabel(audienceFilter) }}】</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标RTP">
|
||||
<el-input-number v-model="recalcForm.rtp" :min="0" :max="1" :precision="3" :step="0.05" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="总回合">
|
||||
<el-input-number v-model="recalcForm.totalRounds" :min="50" :step="50" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="固定/收敛回合%">
|
||||
<el-input-number v-model="recalcForm.fixedPct" :min="0" :max="100" :step="5" style="width:40%" @change="onRecalcFixedPctChange" />
|
||||
<span style="margin-left:4px">%</span>
|
||||
<span style="margin:0 4%">/</span>
|
||||
<el-input-number v-model="recalcForm.convergePct" :min="0" :max="100" :step="5" style="width:40%" @change="onRecalcConvergePctChange" />
|
||||
<span style="margin-left:4px">%</span>
|
||||
<div style="color:#909399;font-size:12px">固定% + 收敛% = 100;收敛回合用于一轮内把实时RTP动态拉回目标</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="recalcDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="recalcLoading" @click="submitRecalc">确认重算</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 饰品配置弹窗(跟随列表页当前身份) -->
|
||||
<el-dialog
|
||||
:title="'饰品配置 - (ID:' + currentBox.boxId + ')【' + audienceLabel(audience) + '】'"
|
||||
:visible.sync="bucketVisible"
|
||||
width="760px"
|
||||
append-to-body
|
||||
>
|
||||
<div style="display:flex;align-items:center;margin-bottom:10px">
|
||||
<el-alert type="info" :closable="false" style="flex:1">
|
||||
身份【{{ audienceLabel(audience) }}】 计算回合(固定): <b>{{ bucketTotal }}</b> |
|
||||
收敛回合: <b>{{ convergeRoundsDerived }}</b> | 总回合: <b>{{ totalRoundsDerived }}</b> |
|
||||
计算RTP(池内实际均值): <b>{{ bucketRTP }}%</b> | 目标RTP: <b>{{ currentBucketTargetRtp }}%</b>
|
||||
</el-alert>
|
||||
<el-button type="warning" size="small" icon="el-icon-refresh" style="margin-left:10px;flex-shrink:0" @click="handleBucketRecalc">重算分布</el-button>
|
||||
</div>
|
||||
<el-table v-loading="bucketLoading" :data="bucketList" border max-height="420">
|
||||
<el-table-column prop="bucketId" label="桶ID" width="60" align="center" />
|
||||
<el-table-column label="饰品" min-width="110" align="center">
|
||||
<template slot-scope="scope">{{ (scope.row.bucketName || scope.row.name || '').replace('x', '倍桶') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="倍率范围" min-width="120" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.rangeLow }} - {{ scope.row.rangeHigh }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="期望倍率" min-width="90" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.expectMul }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回合数" min-width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.count" :min="0" :step="10" size="small" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余" min-width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: (scope.row.remaining != null ? scope.row.remaining : scope.row.count) <= 0 ? '#f56c6c' : '#67c23a'}">{{ scope.row.remaining != null ? scope.row.remaining : scope.row.count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="动态收敛回合" min-width="100" align="center">
|
||||
<template slot-scope="scope"><span style="color:#e6a23c">{{ scope.row.convergeCount || 0 }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div slot="footer">
|
||||
<el-button @click="bucketVisible = false">取 消</el-button>
|
||||
<el-button type="success" icon="el-icon-document-add" @click="handleSaveAsTemplate">保存为模板</el-button>
|
||||
<el-button type="primary" @click="saveBuckets">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 模板管理弹窗(跟随列表页当前身份) -->
|
||||
<el-dialog
|
||||
:title="'模板管理 - (ID:' + templateBox.boxId + ')【' + audienceLabel(templateAudience) + '】'"
|
||||
:visible.sync="templateDialogVisible"
|
||||
width="720px"
|
||||
append-to-body
|
||||
>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px">
|
||||
当前为身份【{{ audienceLabel(templateAudience) }}】的模板。模板保存了该箱子该身份的饰品桶回合分布。选择后点【应用】可将该身份桶数量恢复为模板值,并设为抽干补桶依据。
|
||||
</el-alert>
|
||||
<el-table v-if="boxTemplates.length > 0" :data="boxTemplates" border highlight-current-row @current-change="onTemplateRowChange">
|
||||
<el-table-column label="模板名称" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.name }}
|
||||
<el-tag v-if="scope.row.isActive === 1" type="success" size="mini" style="margin-left:6px">生效中</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="饰品数" width="80" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.items ? scope.row.items.length : 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="固定回合" width="90" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.totalRounds }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="保存时间" width="170" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.createTime }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="danger" @click="handleDeleteTemplate(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="该身份暂无模板,请在饰品配置中修改数量后点【保存为模板】。" />
|
||||
<div slot="footer">
|
||||
<el-button @click="templateDialogVisible = false">关 闭</el-button>
|
||||
<el-button type="primary" :disabled="!selectedTemplateRow" @click="handleApplyTemplate">应用选中模板</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getBoxList, saveBox, getBoxBuckets, saveBoxBuckets, calcDistribution, getBoxTemplates, saveBoxTemplate, deleteBoxTemplate, applyBoxTemplate, recalcBatch } from '@/api/game/mahjong/box/api'
|
||||
import { getBucketList } from '@/api/game/mahjong/bucket/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongBox',
|
||||
data() {
|
||||
return {
|
||||
loading: false, recalcLoading: false, saveLoading: false, boxList: [], total: 0,
|
||||
queryParams: { pageNum: 1, pageSize: 10, boxType: null },
|
||||
audienceFilter: 'common',
|
||||
dialogVisible: false, bucketVisible: false, currentBox: {},
|
||||
form: { boxId: null, name: '', bet: 1, boxType: 'normal', rtp: 0.8, status: '1', price: 0, totalRounds: 0, fixedPct: 85, convergePct: 15 },
|
||||
// 重算分布弹窗
|
||||
recalcDialogVisible: false,
|
||||
recalcForm: { onlyCurrent: false, boxType: '', rtp: 0.8, totalRounds: 200, fixedPct: 85, convergePct: 15 },
|
||||
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'white', name: '白号' }],
|
||||
audience: 'common', bucketLoading: false, bucketList: [], bucketAll: [], bucketExpects: {},
|
||||
// 模板
|
||||
templateDialogVisible: false, templateBox: {}, templateAudience: 'common', boxTemplates: [], selectedTemplateRow: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bucketTotal() { return this.bucketList.reduce((s, o) => s + (o.count || 0), 0) },
|
||||
bucketRTP() {
|
||||
const total = this.bucketTotal
|
||||
if (!total || !this.currentBox.bet) return '0.0'
|
||||
let contrib = 0
|
||||
this.bucketList.forEach(o => { const mul = this.bucketExpects[o.bucketId]; if (mul != null) contrib += (o.count || 0) * mul })
|
||||
// 本游戏 costMultiplier ≡ 1(price ≡ bet),此处不再有模式倍率表
|
||||
const cm = Number(this.currentBox.costMultiplier) || 1
|
||||
return (contrib / total / cm * 100).toFixed(1)
|
||||
},
|
||||
convergeRatioVal() {
|
||||
let cr = this.currentBox.rtpConvergeRatio != null ? Number(this.currentBox.rtpConvergeRatio) : 0.15
|
||||
if (!(cr >= 0)) cr = 0.15
|
||||
return Math.min(0.9, Math.max(0, cr))
|
||||
},
|
||||
totalRoundsDerived() { const fixed = this.bucketTotal; return fixed > 0 ? Math.round(fixed / (1 - this.convergeRatioVal)) : 0 },
|
||||
convergeRoundsDerived() { return Math.max(0, this.totalRoundsDerived - this.bucketTotal) },
|
||||
// 当前身份目标RTP(%):优先取该身份分布反推值,无则回退箱子统一值
|
||||
currentBucketTargetRtp() {
|
||||
const m = this.currentBox.targetRtpByAudience
|
||||
const v = m && m[this.audience] != null ? m[this.audience] : null
|
||||
if (v != null) return Number(v).toFixed(1)
|
||||
return this.currentBox.rtp != null ? (this.currentBox.rtp * 100).toFixed(1) : '-'
|
||||
}
|
||||
},
|
||||
created() { this.loadData() },
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loading = true
|
||||
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
|
||||
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
|
||||
getBoxList(params).then(res => {
|
||||
this.boxList = (res.data && res.data.rows) || []
|
||||
this.total = (res.data && res.data.total) || 0
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
openRecalcDialog() {
|
||||
// 默认参数取当前列表第一行的当前身份反推值(无数据时用默认)
|
||||
const row = this.boxList[0] || {}
|
||||
let rtp = 0.8, totalRounds = 200, convergePct = 15
|
||||
const t = row.targetRtpByAudience && row.targetRtpByAudience[this.audienceFilter]
|
||||
if (t != null) rtp = Number(t) / 100
|
||||
const tr = row.totalRoundsByAudience && row.totalRoundsByAudience[this.audienceFilter]
|
||||
if (tr != null) totalRounds = Number(tr)
|
||||
if (row.rtpConvergeRatio != null) convergePct = Math.round(Number(row.rtpConvergeRatio) * 100)
|
||||
this.recalcForm = { onlyCurrent: false, boxType: '', rtp, totalRounds, fixedPct: 100 - convergePct, convergePct }
|
||||
this.recalcDialogVisible = true
|
||||
},
|
||||
onRecalcFixedPctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.recalcForm.fixedPct = val; this.recalcForm.convergePct = 100 - val },
|
||||
onRecalcConvergePctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.recalcForm.convergePct = val; this.recalcForm.fixedPct = 100 - val },
|
||||
submitRecalc() {
|
||||
const f = this.recalcForm
|
||||
const params = {
|
||||
boxType: f.boxType || null,
|
||||
audience: f.onlyCurrent ? this.audienceFilter : null,
|
||||
rtp: f.rtp,
|
||||
totalRounds: f.totalRounds,
|
||||
convergeRatio: f.convergePct / 100
|
||||
}
|
||||
this.recalcLoading = true
|
||||
recalcBatch(params).then(() => {
|
||||
this.$message.success('重算完成')
|
||||
this.recalcDialogVisible = false
|
||||
this.loadData()
|
||||
}).catch(e => this.$message.error('重算失败: ' + (e.message || e)))
|
||||
.finally(() => { this.recalcLoading = false })
|
||||
},
|
||||
handleEdit(row) {
|
||||
const convergePct = row.rtpConvergeRatio != null ? Math.round(row.rtpConvergeRatio * 100) : 15
|
||||
// 目标RTP 初始值 = 当前身份分布反推的目标(百分点/100),避免显示成其他身份的值
|
||||
let rtp = row.rtp != null ? row.rtp : 0.8
|
||||
const t = row.targetRtpByAudience && row.targetRtpByAudience[this.audienceFilter]
|
||||
if (t != null) rtp = Number(t) / 100
|
||||
// 总回合 初始值 = 当前身份分布反推的一轮总回合(身份级独立)
|
||||
let totalRounds = row.totalRounds || 0
|
||||
const tr = row.totalRoundsByAudience && row.totalRoundsByAudience[this.audienceFilter]
|
||||
if (tr != null) totalRounds = Number(tr)
|
||||
this.form = { ...row, totalRounds, rtp, status: row.status || '1', convergePct, fixedPct: 100 - convergePct }
|
||||
this.dialogVisible = true
|
||||
},
|
||||
onFixedPctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.form.fixedPct = val; this.form.convergePct = 100 - val },
|
||||
onConvergePctChange(v) { const val = Math.min(100, Math.max(0, v || 0)); this.form.convergePct = val; this.form.fixedPct = 100 - val },
|
||||
submitForm() {
|
||||
const convergeRatio = (this.form.convergePct != null ? this.form.convergePct : 15) / 100
|
||||
// totalRounds 会同时回写 total_rounds,保证 cycleN 与实际池子一致
|
||||
const data = { boxId: this.form.boxId, name: this.form.name, bet: this.form.bet, boxType: this.form.boxType, rtp: this.form.rtp, status: this.form.status, rtpConvergeRatio: convergeRatio, totalRounds: this.form.totalRounds }
|
||||
this.saveLoading = true
|
||||
saveBox(data).then(res => {
|
||||
const boxId = this.form.boxId || (res.data && res.data.boxId)
|
||||
if (!boxId) { this.saveLoading = false; this.$message.success('保存成功'); this.dialogVisible = false; this.loadData(); return }
|
||||
// 保存后只重算当前身份
|
||||
calcDistribution({ boxId, audience: this.audienceFilter, totalRounds: this.form.totalRounds, rtp: this.form.rtp, convergeRatio })
|
||||
.then(() => { this.$message.success('修改成功,已重算【' + this.audienceLabel(this.audienceFilter) + '】身份分布') })
|
||||
.catch(() => { this.$message.success('修改成功(分布计算失败,可稍后用"重算分布"重试)') })
|
||||
.finally(() => { this.saveLoading = false; this.dialogVisible = false; this.loadData() })
|
||||
}).catch(() => { this.saveLoading = false })
|
||||
},
|
||||
handleBuckets(row) {
|
||||
this.currentBox = row
|
||||
this.audience = this.audienceFilter // 跟随列表页当前身份
|
||||
getBucketList().then(bkRes => {
|
||||
this.bucketAll = (bkRes.data && bkRes.data.list) ? bkRes.data.list : []
|
||||
this.bucketExpects = {}
|
||||
this.bucketAll.forEach(o => { this.bucketExpects[o.id] = Number(o.expectMul) })
|
||||
this.bucketVisible = true
|
||||
this._refreshBucketList()
|
||||
})
|
||||
},
|
||||
_refreshBucketList() {
|
||||
this.bucketLoading = true
|
||||
getBoxBuckets(this.currentBox.boxId, this.audience).then(res => {
|
||||
const items = (res.data && res.data.list) ? res.data.list : []
|
||||
// 「计算RTP」必须与后台「目标RTP」同一口径 = 该桶**池内实际均值**(AVG(actual_mul)),
|
||||
// 否则会拿桶定义的名义 expectMul(区间中点)来算,两个数必然对不上。
|
||||
// 池子里还没有该桶的回合时,后端会回退名义值,这里保持同样回退。
|
||||
const pe = (res.data && res.data.poolExpect) || {}
|
||||
const expects = {}
|
||||
this.bucketAll.forEach(o => {
|
||||
expects[o.id] = pe[o.id] != null ? Number(pe[o.id]) : Number(o.expectMul)
|
||||
})
|
||||
this.bucketExpects = expects
|
||||
const map = {}
|
||||
items.forEach(it => { map[it.bucketId] = { count: it.count, remaining: it.remaining != null ? it.remaining : (it.count - (it.used || 0)), convergeCount: it.convergeCount } })
|
||||
// 只有 normal 一种模式:桶范围恒为 1~16
|
||||
this.bucketList = this.bucketAll.filter(o => o.id >= 1 && o.id <= 16)
|
||||
.map(o => ({ ...o, count: (map[o.id] && map[o.id].count) || 0, remaining: map[o.id] ? map[o.id].remaining : 0, convergeCount: map[o.id] ? map[o.id].convergeCount : 0 }))
|
||||
this.bucketLoading = false
|
||||
}).catch(() => { this.bucketLoading = false })
|
||||
},
|
||||
handleBucketRecalc() {
|
||||
const box = this.currentBox
|
||||
const cr = box.rtpConvergeRatio != null ? box.rtpConvergeRatio : 0.15
|
||||
// 目标RTP 优先用当前身份分布反推值,与"按身份编辑"语义一致
|
||||
const t = box.targetRtpByAudience && box.targetRtpByAudience[this.audience] != null ? Number(box.targetRtpByAudience[this.audience]) / 100 : (box.rtp || 0.8)
|
||||
// 总回合 用当前身份反推值(身份级独立)
|
||||
const tr = box.totalRoundsByAudience && box.totalRoundsByAudience[this.audience] != null ? Number(box.totalRoundsByAudience[this.audience]) : (box.totalRounds || 200)
|
||||
calcDistribution({ boxId: box.boxId, audience: this.audience, totalRounds: tr, rtp: t, convergeRatio: cr }).then(() => {
|
||||
this.$message.success('分布已重算并保存')
|
||||
this._refreshBucketList()
|
||||
this.loadData()
|
||||
}).catch(() => { this.$message.error('重算失败') })
|
||||
},
|
||||
saveBuckets() {
|
||||
const items = this.bucketList.filter(o => o.count > 0).map(o => ({ bucketId: o.id, count: o.count }))
|
||||
saveBoxBuckets({ boxId: this.currentBox.boxId, audience: this.audience, items }).then(() => {
|
||||
this.$message.success('保存成功')
|
||||
this.bucketVisible = false
|
||||
this.loadData()
|
||||
})
|
||||
},
|
||||
handleSaveAsTemplate() {
|
||||
const items = this.bucketList.filter(o => o.count > 0).map(o => ({ bucketId: o.id, count: o.count }))
|
||||
if (items.length === 0) return this.$message.warning('当前没有配置任何桶回合数,无法保存模板')
|
||||
this.$prompt('请输入模板名称(同一箱子+身份内不可重复)', '保存为模板【' + this.audienceLabel(this.audience) + '】', { inputPattern: /\S+/, inputErrorMessage: '名称不能为空' })
|
||||
.then(({ value }) => {
|
||||
saveBoxTemplate({ boxId: this.currentBox.boxId, audience: this.audience, name: value.trim(), items })
|
||||
.then(() => this.$message.success('模板【' + value.trim() + '】已保存'))
|
||||
.catch(e => this.$message.error('保存失败: ' + (e.message || e)))
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 模板管理
|
||||
openTemplateManager(row) { this.templateBox = row; this.templateAudience = this.audienceFilter; this.selectedTemplateRow = null; this.templateDialogVisible = true; this._loadTemplates() },
|
||||
_loadTemplates() {
|
||||
getBoxTemplates(this.templateBox.boxId, this.templateAudience).then(res => { this.boxTemplates = (res.data && res.data.list) || [] }).catch(() => { this.boxTemplates = [] })
|
||||
},
|
||||
onTemplateRowChange(row) { this.selectedTemplateRow = row },
|
||||
handleDeleteTemplate(tpl) {
|
||||
this.$confirm('确定删除模板【' + tpl.name + '】吗?', '提示', { type: 'warning' }).then(() => {
|
||||
deleteBoxTemplate(tpl.id).then(() => { this.selectedTemplateRow = null; this.$message.success('已删除'); this._loadTemplates() })
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleApplyTemplate() {
|
||||
if (!this.selectedTemplateRow) return
|
||||
const tpl = this.selectedTemplateRow
|
||||
this.$confirm('将模板【' + tpl.name + '】应用到身份【' + this.audienceLabel(this.templateAudience) + '】,并设为抽干补桶依据,确认?', '应用模板', { type: 'warning' }).then(() => {
|
||||
applyBoxTemplate({ templateId: tpl.id }).then(() => {
|
||||
this.$message.success('模板已应用,桶分布已更新')
|
||||
this.templateDialogVisible = false
|
||||
if (this.bucketVisible && this.currentBox.boxId === this.templateBox.boxId && this.audience === this.templateAudience) this._refreshBucketList()
|
||||
this.loadData()
|
||||
}).catch(e => this.$message.error('应用失败: ' + (e.message || e)))
|
||||
}).catch(() => {})
|
||||
},
|
||||
audienceLabel(key) { const a = this.audiences.find(x => x.key === key); return a ? a.name : key },
|
||||
// 按当前身份筛选取值(后端一次返回4身份)
|
||||
targetRtp(row) {
|
||||
const m = row.targetRtpByAudience
|
||||
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : null
|
||||
},
|
||||
actualRtp(row) {
|
||||
const m = row.actualRtpByAudience
|
||||
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : null
|
||||
},
|
||||
// 当前身份一轮总回合(身份级独立)
|
||||
totalRoundsOf(row) {
|
||||
const m = row.totalRoundsByAudience
|
||||
return m && m[this.audienceFilter] != null ? m[this.audienceFilter] : (row.totalRounds != null ? row.totalRounds : null)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:16px">
|
||||
16 档倍率桶(0x~500x)的定义与落桶区间。单局封顶 500x,所以最高一档的上限就是 500。
|
||||
分布由 Python 预生成落库,此处仅供参考查看。
|
||||
</el-alert>
|
||||
<el-table v-loading="loading" :data="list" border>
|
||||
<el-table-column prop="id" label="桶ID" width="80" align="center" />
|
||||
<el-table-column label="名称" min-width="140" align="center">
|
||||
<template slot-scope="scope">{{ (scope.row.name || '').replace('x', '倍桶') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expectMul" label="期望倍率" min-width="120" align="center" />
|
||||
<el-table-column prop="rangeLow" label="落桶下限" min-width="120" align="center" />
|
||||
<el-table-column prop="rangeHigh" label="落桶上限" min-width="120" align="center" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getBucketList } from '@/api/game/mahjong/bucket/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongBucket',
|
||||
data() { return { loading: false, list: [] } },
|
||||
created() {
|
||||
this.loading = true
|
||||
getBucketList().then(res => {
|
||||
this.list = (res.data && res.data.list) || []
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="16" style="margin-bottom:16px">
|
||||
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>累计投注</small><h3>{{ summary.totalCost || 0 }}</h3></div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>累计赢奖</small><h3>{{ summary.totalWin || 0 }}</h3></div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>整体RTP</small><h3 :style="{color: rtp > 100 ? 'red' : 'green'}">{{ rtp }}%</h3></div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><div class="stat"><small>总局数</small><h3>{{ summary.totalSpins || 0 }}</h3></div></el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<el-date-picker v-model="queryParams.dateStart" type="date" placeholder="开始日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
|
||||
<span style="margin:0 4px">至</span>
|
||||
<el-date-picker v-model="queryParams.dateEnd" type="date" placeholder="结束日期" value-format="yyyy-MM-dd" style="width:150px;margin-right:8px" />
|
||||
<el-button type="primary" icon="el-icon-search" @click="loadAll">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border>
|
||||
<el-table-column prop="date" label="日期" min-width="120" align="center" />
|
||||
<el-table-column prop="totalCost" label="总投注" min-width="120" align="center" />
|
||||
<el-table-column prop="totalWin" label="总赢奖" min-width="120" align="center" />
|
||||
<el-table-column prop="totalSpins" label="总局数" min-width="90" align="center" />
|
||||
<el-table-column prop="playerCount" label="玩家数" min-width="90" align="center" />
|
||||
<el-table-column label="RTP" min-width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: scope.row.totalCost > 0 && scope.row.totalWin / scope.row.totalCost > 1 ? 'red' : 'green'}">
|
||||
{{ scope.row.totalCost > 0 ? (Math.round(scope.row.totalWin / scope.row.totalCost * 10000) / 100).toFixed(2) + '%' : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top:16px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="total"
|
||||
:page-size.sync="queryParams.pageSize"
|
||||
:current-page.sync="queryParams.pageNum"
|
||||
:page-sizes="[10,20,50]"
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getWinList, getWinSummary } from '@/api/game/mahjong/dailywin/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongDailyWin',
|
||||
data() {
|
||||
return {
|
||||
loading: false, list: [], total: 0, summary: {},
|
||||
queryParams: { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rtp() {
|
||||
const c = Number(this.summary.totalCost) || 0
|
||||
const w = Number(this.summary.totalWin) || 0
|
||||
return c > 0 ? (Math.round(w / c * 10000) / 100).toFixed(2) : '0.00'
|
||||
}
|
||||
},
|
||||
created() { this.loadAll() },
|
||||
methods: {
|
||||
loadAll() { this.loadSummary(); this.loadData() },
|
||||
loadSummary() {
|
||||
getWinSummary({ dateStart: this.queryParams.dateStart, dateEnd: this.queryParams.dateEnd }).then(res => {
|
||||
this.summary = res.data || {}
|
||||
})
|
||||
},
|
||||
loadData() {
|
||||
this.loading = true
|
||||
getWinList(this.queryParams).then(res => {
|
||||
this.list = (res.data && res.data.rows) || []
|
||||
this.total = (res.data && res.data.total) || 0
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
handleReset() {
|
||||
this.queryParams = { pageNum: 1, pageSize: 10, dateStart: new Date().toISOString().slice(0, 10), dateEnd: null }
|
||||
this.loadAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat { text-align: center; }
|
||||
.stat small { color: #909399; }
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px">
|
||||
返奖饰品按"结算赢额"匹配返奖区间展示。区间需连续覆盖;未命中时用兜底饰品。
|
||||
保存时会同步写入 <b>game_mahjong_prize_range</b>(按 prizeId 唯一)。
|
||||
</el-alert>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border>
|
||||
<el-table-column prop="id" label="ID" width="60" align="center" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="品质" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.grade" size="small" :type="gradeType(scope.row.grade)" :style="gradeStyle(scope.row.grade)">{{ gradeLabel(scope.row.grade) }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="饰品图片" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image v-if="scope.row.imageUrl" :src="getImg(scope.row.imageUrl)" style="width:40px;height:40px" fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="品质图片" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image v-if="scope.row.levelImg" :src="getImg(scope.row.levelImg)" style="width:40px;height:40px" fit="contain" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="磨损" width="110" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.exteriorName || '崭新出厂' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="兜底" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.isFallback === 1" type="danger" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="返奖区间" min-width="160" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.winLow != null">{{ scope.row.winLow }} ~ {{ scope.row.winHigh }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" width="70" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.sort }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
title="编辑奖品"
|
||||
:visible.sync="dialogVisible"
|
||||
width="480px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="form" :model="form" label-width="80px">
|
||||
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="品质">
|
||||
<el-select v-model="form.grade" style="width:100%">
|
||||
<el-option label="1 - 金" :value="1" />
|
||||
<el-option label="2 - 红" :value="2" />
|
||||
<el-option label="3 - 紫" :value="3" />
|
||||
<el-option label="4 - 蓝" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="饰品图片">
|
||||
<ImageUpload v-model="form.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="磨损">
|
||||
<el-select v-model="form.exteriorName" placeholder="请选择磨损度" clearable style="width:100%">
|
||||
<el-option label="崭新出厂" value="崭新出厂" />
|
||||
<el-option label="略有磨损" value="略有磨损" />
|
||||
<el-option label="久经沙场" value="久经沙场" />
|
||||
<el-option label="破损不堪" value="破损不堪" />
|
||||
<el-option label="战痕累累" value="战痕累累" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="兜底饰品">
|
||||
<el-switch v-model="form.isFallback" :active-value="1" :inactive-value="0" active-text="是" inactive-text="否" />
|
||||
<div style="color:#909399;font-size:12px;margin-top:4px">未命中区间时随机选择一个兜底饰品返奖</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="返奖区间">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<el-input-number v-model="form.winLow" :min="0" :precision="2" :step="0.01" controls-position="right" placeholder="最小值" style="flex:1" />
|
||||
<span>~</span>
|
||||
<el-input-number v-model="form.winHigh" :min="0" :precision="2" :step="0.01" controls-position="right" placeholder="最大值" style="flex:1" />
|
||||
</div>
|
||||
<div style="color:#909399;font-size:12px;margin-top:4px">玩家结算赢额落在该区间即获得此饰品(最高到 500x 封顶对应的赢额)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort" :min="0" :step="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { prizeList, prizeSave } from '@/api/game/mahjong/prize/api'
|
||||
import ImageUpload from '@/components/ImageUpload'
|
||||
|
||||
export default {
|
||||
name: 'MahjongPrize',
|
||||
components: { ImageUpload },
|
||||
data() {
|
||||
return { loading: false, list: [], dialogVisible: false, form: {}}
|
||||
},
|
||||
created() { this.getList() },
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true
|
||||
prizeList().then(res => { this.list = (res.data && res.data.list) || []; this.loading = false }).catch(() => { this.loading = false })
|
||||
},
|
||||
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : '' },
|
||||
// 品质配色与饰品品质一致:1金/2红/3紫/4蓝
|
||||
gradeType(g) { const m = { 1: 'warning', 2: 'danger', 3: '', 4: '' }; return m[g] === undefined ? 'info' : m[g] },
|
||||
gradeStyle(g) { return g === 3 ? { color: '#a16ee8', background: '#f7f1fe', borderColor: '#e4d5f9' } : null },
|
||||
gradeLabel(g) { return ({ 1: '金', 2: '红', 3: '紫', 4: '蓝' })[g] || ('品质' + g) },
|
||||
handleEdit(row) { this.form = { ...row }; this.dialogVisible = true },
|
||||
submitForm() {
|
||||
if (!this.form.name || !this.form.name.trim()) return this.$message.warning('名称不能为空')
|
||||
prizeSave(this.form).then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.getList() })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :inline="true" class="demo-form-inline">
|
||||
<el-form-item label="玩家ID">
|
||||
<el-input v-model="queryParams.userId" placeholder="输入玩家ID" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型">
|
||||
<el-select v-model="queryParams.audience" placeholder="全部" clearable style="width:130px" @change="handleQuery">
|
||||
<el-option v-for="a in audiences" :key="a.key" :label="a.name" :value="a.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="userId" label="玩家ID" width="90" align="center" />
|
||||
<el-table-column label="用户类型" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="audienceTagType(scope.row.audience)" size="small">{{ audienceLabel(scope.row.audience) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="boxId" label="箱子" width="70" align="center" />
|
||||
<el-table-column prop="prizeName" label="饰品名称" min-width="150" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="饰品图片" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-image v-if="scope.row.prizeImageUrl" :src="getImg(scope.row.prizeImageUrl)" style="width:40px;height:40px" fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cost" label="花费" width="90" align="center" />
|
||||
<el-table-column prop="settledWin" label="结算赢额" width="100" align="center" />
|
||||
<el-table-column label="播放状态" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip :content="settleTips[scope.row.settleSource] || '历史值'" placement="top">
|
||||
<el-tag :type="scope.row.settleSource === 'start' ? 'warning' : 'success'" size="small">
|
||||
{{ settleLabels[scope.row.settleSource] || scope.row.settleSource || '—' }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="时间" width="160" align="center" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top:16px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="total"
|
||||
:page-size.sync="queryParams.pageSize"
|
||||
:current-page.sync="queryParams.pageNum"
|
||||
:page-sizes="[10,20,50]"
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRecordList } from '@/api/game/mahjong/record/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongRecord',
|
||||
data() {
|
||||
return {
|
||||
loading: false, list: [], total: 0,
|
||||
queryParams: { pageNum: 1, pageSize: 10, userId: null, audience: null },
|
||||
// 身份 4 种:白号独立库存、不进真实经济
|
||||
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'white', name: '白号' }],
|
||||
// 播放状态:start=开局即结算(奖已入背包)但动画没播完;与"结算赢额"不矛盾
|
||||
settleLabels: { start: '未播完', finish: '已播完', reconnect: '重连补结', scheduled: '定时兜底', force_settle: '强制结算' },
|
||||
settleTips: {
|
||||
start: '开局就已结算:钱和饰品当场定下并直接进背包(防断网丢奖)。只是整局动画没播到末帧(玩家中途退出/断网),客户端战绩暂不展示,超 30 分钟兜底放行',
|
||||
finish: '整局已播到末帧,客户端战绩正常展示(站内信也是此刻才发)'
|
||||
}
|
||||
}
|
||||
},
|
||||
created() { this.loadData() },
|
||||
methods: {
|
||||
audienceLabel(key) {
|
||||
const a = this.audiences.find(x => x.key === key)
|
||||
return a ? a.name : (key || '—')
|
||||
},
|
||||
audienceTagType(key) {
|
||||
return { common: '', anchor: 'success', white: 'warning' }[key] || 'info'
|
||||
},
|
||||
loadData() {
|
||||
this.loading = true
|
||||
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
|
||||
if (this.queryParams.userId) params.userId = this.queryParams.userId
|
||||
if (this.queryParams.audience) params.audience = this.queryParams.audience
|
||||
getRecordList(params).then(res => {
|
||||
this.list = (res.data && res.data.rows) || []
|
||||
this.total = (res.data && res.data.total) || 0
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.loadData() },
|
||||
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, userId: null, audience: null }; this.loadData() },
|
||||
getImg(u) { return u ? (u.startsWith('http') ? u : (process.env.VUE_APP_BASE_API || '') + u) : '' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div class="app-container reward-pool-page">
|
||||
<div class="page-head">
|
||||
<div class="page-head-row">
|
||||
<el-button type="text" size="mini" :icon="showGuide ? 'el-icon-arrow-up' : 'el-icon-question'" @click="showGuide = !showGuide">
|
||||
{{ showGuide ? '收起说明' : '配置说明' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<p class="head-tip">
|
||||
奖池按【箱子 × 身份】独立:每箱每身份一套水位 = 该身份箱内下注 − 箱内返奖 + 该箱该身份人工调整;开局按「(本箱该身份水位 + 本局花费) × 目标RTP」算赔付上限,只有预期赔付不超上限的回合才可被抽中。白号独立库存,水位仅统计与人工调整,不受上限约束(纯概率),不进真实经济。
|
||||
</p>
|
||||
<div class="audience-row">
|
||||
<span class="aud-label">身份:</span>
|
||||
<el-radio-group v-model="audience" size="small" @change="switchAudience">
|
||||
<el-radio-button v-for="a in audiences" :key="a.key" :label="a.key">{{ a.name }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="aud-tip">三个身份(普通/主播/白号)各自独立水位、独立注水/抽水、独立调整历史。</span>
|
||||
</div>
|
||||
<div v-show="showGuide" class="guide-panel">
|
||||
<div class="guide-col">
|
||||
<h5><i class="el-icon-info" /> 机制说明</h5>
|
||||
<ul>
|
||||
<li>每箱每身份独立水位 = 该身份箱内累计下注 − 累计返奖 + 人工调整;三个身份互不串池。</li>
|
||||
<li>白号(audience=white)水位独立统计,但按设计不受水位约束,白号出奖=配比即爆率,注水/抽水只影响记录。</li>
|
||||
<li>抽回合两阶段:固定阶段按分布抽取(确定性);轮末收敛阶段按实时RTP偏差定向选桶纠偏。</li>
|
||||
<li>水位随下注升高、随返奖降低;顶部总池为当前身份全站汇总,仅展示不可调。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="guide-col">
|
||||
<h5><i class="el-icon-setting" /> 如何配置</h5>
|
||||
<ul>
|
||||
<li>新箱上线或水位偏低:建议批量注水一轮花费的 60%~80%(接近目标RTP),相当于提前垫付整轮返奖预算,保证高倍桶全程可正常抽出。</li>
|
||||
<li>日常运营小幅补水:比例 5%~20% 即可;某箱长期不出高倍时检查该箱水位,接近 0 或为负则单箱注水。</li>
|
||||
<li>返奖偏高想收紧:批量或单箱抽水;水位为负时只会出最低倍,请谨慎大额抽水。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="guide-col">
|
||||
<h5><i class="el-icon-s-operation" /> 批量调整规则</h5>
|
||||
<ul>
|
||||
<li>基数 = 该箱抽干一轮的总花费 = 赌注 × 总回合数。<b>本游戏无投注倍数</b>(price ≡ 赌注)。</li>
|
||||
<li>每箱调整金额 = 基数 × 比例。例:赌注 1、1000 回合 → 基数 1000,注水 10% = +100。</li>
|
||||
<li>比例支持 0~1000%;基数为 0 的箱子自动跳过;批量操作记一条汇总历史可审计(调整原因必填,会写进调整历史)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="guide-col">
|
||||
<h5><i class="el-icon-data-line" /> 两阶段RTP</h5>
|
||||
<ul>
|
||||
<li>本轮实时RTP / 已抽数反映当前轮收敛进度;一轮抽满 N 回合后自动重置账本开新轮。</li>
|
||||
<li>收敛占比越高,轮末纠偏能力越强;本游戏只有普通旋转,单局封顶 500x。</li>
|
||||
<li>总回合数 N 与收敛占比在「箱子配置 → 编辑」中配置。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="sc-icon sc-blue"><i class="el-icon-coin" /></div>
|
||||
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总下注额</small><h3>{{ fmt(pool.totalBet) }}</h3></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="sc-icon sc-orange"><i class="el-icon-present" /></div>
|
||||
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总返奖额</small><h3>{{ fmt(pool.totalWin) }}</h3></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="sc-icon sc-cyan"><i class="el-icon-wallet" /></div>
|
||||
<div class="sc-body"><small>{{ audienceLabel(audience) }}·总池余额</small><h3 :style="{color: Number(pool.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(pool.balance) }}</h3></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-card">
|
||||
<div class="sc-icon sc-green"><i class="el-icon-data-line" /></div>
|
||||
<div class="sc-body"><small>{{ audienceLabel(audience) }}·实时RTP</small><h3 :style="{color: rtpPct(pool.rtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ rtpPct(pool.rtp).toFixed(1) }}%</h3></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="batch-card">
|
||||
<div slot="header">
|
||||
<b>批量调整(按一轮花费比例)</b>
|
||||
<span class="sub-tip">身份【{{ audienceLabel(audience) }}】:基数 = 赌注 × 总回合数(随箱子配置动态变化,本游戏无投注倍数)</span>
|
||||
</div>
|
||||
<el-form :inline="true" @submit.native.prevent>
|
||||
<!-- 注:skin.scss 全局隐藏了 inline 表单的 label,所以字段名写成 span,别用 label 属性 -->
|
||||
<el-form-item>
|
||||
<span class="fld-label">调整比例</span>
|
||||
<el-input-number v-model="batchForm.ratio" :min="0.01" :max="1000" :precision="2" :step="5" :controls="false" style="width:120px" placeholder="如 10" />
|
||||
<span style="margin-left:4px">%</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span class="fld-label">调整原因<span class="req">*</span></span>
|
||||
<el-input v-model="batchForm.remark" maxlength="80" placeholder="必填,如:活动补水" style="width:200px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-popconfirm :title="'按【' + audienceLabel(audience) + '】各箱一轮花费 ' + (batchForm.ratio || 0) + '% 注水,预估共 +' + fmt(estBatchTotal) + ',确定?'" @confirm="submitBatch(1)">
|
||||
<el-button slot="reference" type="success" plain icon="el-icon-top" :loading="batchLoading" :disabled="!batchForm.ratio">批量注水</el-button>
|
||||
</el-popconfirm>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-popconfirm :title="'按【' + audienceLabel(audience) + '】各箱一轮花费 ' + (batchForm.ratio || 0) + '% 抽水,预估共 -' + fmt(estBatchTotal) + ',确定?'" @confirm="submitBatch(2)">
|
||||
<el-button slot="reference" type="danger" plain icon="el-icon-bottom" :loading="batchLoading" :disabled="!batchForm.ratio">批量抽水</el-button>
|
||||
</el-popconfirm>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-popconfirm :title="'重置将把身份【' + audienceLabel(audience) + '】所有箱子的注水/抽水累计值归 0,只保留真实水位(下注−返奖),确定?'" @confirm="submitReset">
|
||||
<el-button slot="reference" type="warning" plain icon="el-icon-refresh-left" :loading="resetLoading">重置调整</el-button>
|
||||
</el-popconfirm>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="batchForm.ratio">
|
||||
<span class="est-text">预估总额:{{ fmt(estBatchTotal) }}({{ boxes.length }} 个箱子)</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="section-head">
|
||||
<h4 style="margin:0">箱子奖池 · 身份【{{ audienceLabel(audience) }}】</h4>
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh" @click="loadConfig">刷新</el-button>
|
||||
</div>
|
||||
<el-table v-loading="boxLoading" :data="pagedBoxes" size="small" border stripe>
|
||||
<el-table-column prop="boxId" label="ID" width="60" align="center" />
|
||||
<el-table-column prop="name" label="箱子" min-width="140" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="赌注" width="80" align="center">
|
||||
<template slot-scope="scope">{{ fmt(scope.row.bet) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="一轮花费(基数)" width="130" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div>{{ fmt(scope.row.cycleCost) }}</div>
|
||||
<div style="font-size:11px;color:#909399">{{ fmt(scope.row.costPerRound) }} × {{ scope.row.cycleRounds || '—' }}回合</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="箱内下注" min-width="100" align="center">
|
||||
<template slot-scope="scope">{{ fmt(scope.row.totalBet) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="箱内返奖" min-width="100" align="center">
|
||||
<template slot-scope="scope">{{ fmt(scope.row.totalWin) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人工调整" min-width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: signColor(scope.row.adjust)}">{{ fmt(scope.row.adjust) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="水位余额" min-width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<b :style="{color: Number(scope.row.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(scope.row.balance) }}</b>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="累计RTP" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: rtpPct(scope.row.rtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ scope.row.rtp == null ? '—' : rtpPct(scope.row.rtp).toFixed(1) + '%' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本轮RTP/进度" min-width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div :style="{color: rtpPct(scope.row.cycleRtp) > 100 ? '#f56c6c' : '#67c23a'}">{{ scope.row.cycleRtp == null ? '—' : rtpPct(scope.row.cycleRtp).toFixed(1) + '%' }}</div>
|
||||
<div style="font-size:11px;color:#909399">{{ scope.row.cycleDrawn || 0 }} / {{ scope.row.totalRounds || '—' }} 回合</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收敛占比" width="80" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.convergeRatio == null ? '—' : (Number(scope.row.convergeRatio) * 100).toFixed(0) + '%' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip content="注水(增加水位)" placement="top">
|
||||
<el-button circle size="mini" type="success" plain icon="el-icon-top" @click="openAdjust(scope.row, 1)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="抽水(降低水位)" placement="top">
|
||||
<el-button circle size="mini" type="danger" plain icon="el-icon-bottom" @click="openAdjust(scope.row, 2)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
style="margin-top:12px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="boxes.length"
|
||||
:page-size.sync="boxSize"
|
||||
:current-page.sync="boxPage"
|
||||
:page-sizes="[10,20,30,50]"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
:title="'调整水位【' + audienceLabel(audience) + '】- ' + (form.boxName || '')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="440px"
|
||||
append-to-body
|
||||
>
|
||||
<el-form label-width="90px" @submit.native.prevent>
|
||||
<el-form-item label="调整类型">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio-button :label="1">注水(增加)</el-radio-button>
|
||||
<el-radio-button :label="2">抽水(减少)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前水位">
|
||||
<b :style="{color: Number(form.balance) < 0 ? '#f56c6c' : '#409eff'}">{{ fmt(form.balance) }}</b>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整金额">
|
||||
<el-input-number v-model="form.amount" :min="0.01" :precision="2" :step="100" :controls="false" style="width:100%" placeholder="请输入金额" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.amount" label="调整后水位">
|
||||
<b :style="{color: afterBalance < 0 ? '#f56c6c' : '#67c23a'}">{{ fmt(afterBalance) }}</b>
|
||||
</el-form-item>
|
||||
<el-form-item label="调整原因" required>
|
||||
<el-input v-model="form.remark" maxlength="100" placeholder="必填,如:活动补水" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button :type="form.type === 1 ? 'success' : 'danger'" :loading="submitting" @click="submit">确定{{ form.type === 1 ? '注水' : '抽水' }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<div class="section-head">
|
||||
<h4 style="margin:0">调整历史 · 身份【{{ audienceLabel(audience) }}】</h4>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" border size="small">
|
||||
<el-table-column prop="createTime" label="时间" min-width="150" align="center" />
|
||||
<el-table-column label="身份" width="80" align="center">
|
||||
<template slot-scope="scope"><el-tag size="mini">{{ audienceLabel(scope.row.audience) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="boxName" label="箱子" min-width="130" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.type === 1 ? 'success' : (scope.row.type === 2 ? 'danger' : 'warning')" size="mini">
|
||||
{{ scope.row.type === 1 ? '注水' : (scope.row.type === 2 ? '抽水' : '重置') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: scope.row.type === 1 ? '#67c23a' : (scope.row.type === 2 ? '#f56c6c' : '#e6a23c')}">
|
||||
{{ scope.row.type === 1 ? '+' : (scope.row.type === 2 ? '-' : '') }}{{ fmt(scope.row.amount) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整前余额" min-width="110" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.balanceBefore == null ? '—' : fmt(scope.row.balanceBefore) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调整后余额" min-width="110" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.balanceAfter == null ? '—' : fmt(scope.row.balanceAfter) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总池余额" min-width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<b :style="{color: Number(scope.row.poolBalance) < 0 ? '#f56c6c' : '#409eff'}">{{ scope.row.poolBalance == null ? '—' : fmt(scope.row.poolBalance) }}</b>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" min-width="90" align="center" />
|
||||
<el-table-column label="备注" min-width="160" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span :style="{color: scope.row.remark ? '#303133' : '#c0c4cc'}">{{ scope.row.remark || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top:16px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="total"
|
||||
:page-size.sync="queryParams.size"
|
||||
:current-page.sync="queryParams.page"
|
||||
:page-sizes="[10,20,50]"
|
||||
@size-change="loadLogs"
|
||||
@current-change="loadLogs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPoolConfig, adjustPool, adjustPoolBatch, adjustPoolReset, getPoolLogs } from '@/api/game/mahjong/rewardPool/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongRewardPool',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
boxLoading: false,
|
||||
submitting: false,
|
||||
batchLoading: false,
|
||||
resetLoading: false,
|
||||
dialogVisible: false,
|
||||
showGuide: false,
|
||||
audience: 'common',
|
||||
audiences: [{ key: 'common', name: '普通' }, { key: 'anchor', name: '主播' }, { key: 'white', name: '白号' }],
|
||||
pool: { totalBet: 0, totalWin: 0, adjust: 0, balance: 0, rtp: 0 },
|
||||
boxes: [],
|
||||
list: [],
|
||||
total: 0,
|
||||
boxPage: 1,
|
||||
boxSize: 10,
|
||||
form: { boxId: undefined, boxName: '', balance: 0, type: 1, amount: undefined, remark: '' },
|
||||
batchForm: { ratio: undefined, remark: '' },
|
||||
queryParams: { page: 1, size: 10, audience: 'common' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pagedBoxes() {
|
||||
const start = (this.boxPage - 1) * this.boxSize
|
||||
return this.boxes.slice(start, start + this.boxSize)
|
||||
},
|
||||
estBatchTotal() {
|
||||
const r = Number(this.batchForm.ratio) || 0
|
||||
if (r <= 0) return 0
|
||||
return this.boxes.reduce((s, b) => s + (Number(b.cycleCost) > 0 ? Number(b.cycleCost) * r / 100 : 0), 0)
|
||||
},
|
||||
afterBalance() {
|
||||
const amt = Number(this.form.amount) || 0
|
||||
return Number(this.form.balance) + (this.form.type === 2 ? -amt : amt)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadConfig()
|
||||
this.loadLogs()
|
||||
},
|
||||
methods: {
|
||||
fmt(v) {
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? '0.00' : n.toFixed(2)
|
||||
},
|
||||
rtpPct(v) {
|
||||
const n = Number(v)
|
||||
return isNaN(n) ? 0 : n * 100
|
||||
},
|
||||
signColor(v) {
|
||||
const n = Number(v)
|
||||
return n < 0 ? '#f56c6c' : (n > 0 ? '#67c23a' : '#303133')
|
||||
},
|
||||
applyConfig(data) {
|
||||
if (!data) return
|
||||
this.pool = data.total || this.pool
|
||||
this.boxes = data.boxes || []
|
||||
},
|
||||
audienceLabel(key) {
|
||||
const a = this.audiences.find(x => x.key === key)
|
||||
return a ? a.name : (key || '普通')
|
||||
},
|
||||
requiredRemark(v) {
|
||||
if (v && v.trim()) return true
|
||||
this.$message.warning('请填写调整原因(备注)')
|
||||
return false
|
||||
},
|
||||
switchAudience() {
|
||||
this.queryParams.audience = this.audience
|
||||
this.queryParams.page = 1
|
||||
this.loadConfig()
|
||||
this.loadLogs()
|
||||
},
|
||||
loadConfig() {
|
||||
this.boxLoading = true
|
||||
getPoolConfig({ audience: this.audience }).then(res => {
|
||||
this.applyConfig(res.data)
|
||||
this.boxLoading = false
|
||||
}).catch(() => { this.boxLoading = false })
|
||||
},
|
||||
loadLogs() {
|
||||
this.loading = true
|
||||
this.queryParams.audience = this.audience
|
||||
getPoolLogs(this.queryParams).then(res => {
|
||||
this.list = (res.data && res.data.rows) || []
|
||||
this.total = (res.data && res.data.total) || 0
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
openAdjust(row, type) {
|
||||
this.form = { boxId: row.boxId, boxName: row.name, audience: this.audience, balance: row.balance, type, amount: undefined, remark: '' }
|
||||
this.dialogVisible = true
|
||||
},
|
||||
submit() {
|
||||
if (!this.form.amount || this.form.amount <= 0) {
|
||||
this.$message.warning('请输入大于 0 的调整金额')
|
||||
return
|
||||
}
|
||||
if (!this.requiredRemark(this.form.remark)) return
|
||||
this.submitting = true
|
||||
adjustPool({ boxId: this.form.boxId, audience: this.audience, type: this.form.type, amount: this.form.amount, remark: this.form.remark.trim() }).then(res => {
|
||||
this.applyConfig(res.data)
|
||||
this.$message.success((this.form.type === 1 ? '注水' : '抽水') + '成功')
|
||||
this.dialogVisible = false
|
||||
this.queryParams.page = 1
|
||||
this.loadLogs()
|
||||
this.submitting = false
|
||||
}).catch(() => { this.submitting = false })
|
||||
},
|
||||
submitBatch(type) {
|
||||
if (!this.batchForm.ratio || this.batchForm.ratio <= 0) {
|
||||
this.$message.warning('请输入大于 0 的调整比例')
|
||||
return
|
||||
}
|
||||
if (!this.requiredRemark(this.batchForm.remark)) return
|
||||
this.batchLoading = true
|
||||
adjustPoolBatch({ audience: this.audience, type, ratio: this.batchForm.ratio, remark: this.batchForm.remark.trim() }).then(res => {
|
||||
this.applyConfig(res.data)
|
||||
this.$message.success(`批量${type === 1 ? '注水' : '抽水'}完成:${res.data.adjusted} 个箱子,共 ${this.fmt(res.data.totalAmount)}`)
|
||||
this.batchForm.ratio = undefined
|
||||
this.batchForm.remark = ''
|
||||
this.queryParams.page = 1
|
||||
this.loadLogs()
|
||||
this.batchLoading = false
|
||||
}).catch(() => { this.batchLoading = false })
|
||||
},
|
||||
submitReset() {
|
||||
if (!this.requiredRemark(this.batchForm.remark)) return
|
||||
this.resetLoading = true
|
||||
adjustPoolReset({ audience: this.audience, remark: this.batchForm.remark.trim() }).then(res => {
|
||||
this.applyConfig(res.data)
|
||||
this.$message.success(`重置完成:${res.data.cleared} 个箱子的人工调整已归零(清除净调整 ${this.fmt(res.data.totalCleared)})`)
|
||||
this.batchForm.remark = ''
|
||||
this.queryParams.page = 1
|
||||
this.loadLogs()
|
||||
this.resetLoading = false
|
||||
}).catch(() => { this.resetLoading = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head { margin-bottom: 14px; }
|
||||
.page-head-row { display: flex; align-items: center; justify-content: space-between; }
|
||||
.head-tip { color: #909399; font-size: 12px; margin: 6px 0 0; }
|
||||
.audience-row { display: flex; align-items: center; margin-top: 10px; }
|
||||
.aud-label { color: #606266; font-size: 13px; margin-right: 6px; }
|
||||
.aud-tip { color: #909399; font-size: 12px; margin-left: 10px; }
|
||||
.guide-panel { display: flex; gap: 16px; background: #f7f9fc; border: 1px solid #e4e9f2; border-radius: 8px; padding: 14px 16px; margin-top: 10px; }
|
||||
.guide-col { flex: 1; min-width: 0; }
|
||||
.guide-col h5 { margin: 0 0 8px; font-size: 13px; color: #303133; }
|
||||
.guide-col h5 i { color: #409eff; margin-right: 4px; }
|
||||
.guide-col ul { margin: 0; padding-left: 16px; }
|
||||
.guide-col li { font-size: 12px; color: #606266; line-height: 1.9; }
|
||||
.stat-row { margin-bottom: 16px; }
|
||||
.stat-card { display: flex; align-items: center; gap: 12px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
|
||||
.sc-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
|
||||
.sc-blue { background: #ecf5ff; color: #409eff; }
|
||||
.sc-orange { background: #fdf6ec; color: #e6a23c; }
|
||||
.sc-cyan { background: #e8f7f7; color: #13a8a8; }
|
||||
.sc-green { background: #f0f9eb; color: #67c23a; }
|
||||
.sc-body small { color: #909399; display: block; }
|
||||
.sc-body h3 { margin: 2px 0 0; font-size: 18px; }
|
||||
.batch-card { margin-bottom: 16px; border-radius: 8px; }
|
||||
.batch-card .sub-tip { color: #909399; font-size: 12px; margin-left: 10px; }
|
||||
.fld-label { color: #606266; font-size: 13px; margin-right: 6px; }
|
||||
.fld-label .req { color: #f56c6c; margin-left: 2px; }
|
||||
.batch-card >>> .el-form-item { margin-bottom: 0; }
|
||||
.est-text { color: #e6a23c; font-size: 13px; }
|
||||
.section-head { display: flex; align-items: center; justify-content: space-between; margin: 16px 0 10px; }
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px">
|
||||
预生成回合池(只读):池里存的是倍率,与赌注无关。<b>actualMul</b> 为本局实际总倍率(已按 500x 封顶);
|
||||
<b>hasFs</b>=是否自然触发免费旋转;<b>nearMiss</b>=悬念盘(基础旋恰好 2 个胡且第 5 轴无胡)。
|
||||
</el-alert>
|
||||
|
||||
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
|
||||
<el-form-item label="回合ID">
|
||||
<el-input v-model="queryParams.roundId" placeholder="输入回合ID" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="roundList" border style="width:100%">
|
||||
<el-table-column prop="roundId" label="回合ID" width="100" align="center" />
|
||||
<el-table-column prop="boxId" label="箱子ID" width="80" align="center" />
|
||||
<el-table-column prop="bucketId" label="饰品桶" width="80" align="center" />
|
||||
<el-table-column prop="actualMul" label="返奖倍率" width="100" align="center" />
|
||||
<el-table-column prop="freeSpinCount" label="免费旋转" width="90" align="center" />
|
||||
<el-table-column prop="totalRotations" label="总波数" width="90" align="center" />
|
||||
<el-table-column label="含FS" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.hasFs === 1" type="success" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="悬念盘" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.nearMiss === 1" type="warning" size="small">是</el-tag>
|
||||
<el-tag v-else type="info" size="small">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="生成时间" min-width="160" align="center" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top:16px;text-align:right"
|
||||
background
|
||||
layout="total, prev, pager, next, sizes"
|
||||
:total="total"
|
||||
:page-size.sync="queryParams.pageSize"
|
||||
:current-page.sync="queryParams.pageNum"
|
||||
:page-sizes="[10,20,50,100]"
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRoundList } from '@/api/game/mahjong/round/api'
|
||||
|
||||
export default {
|
||||
name: 'MahjongRound',
|
||||
data() {
|
||||
return {
|
||||
loading: false, roundList: [], total: 0,
|
||||
queryParams: { pageNum: 1, pageSize: 10, roundId: null, boxType: null }
|
||||
}
|
||||
},
|
||||
created() { this.loadData() },
|
||||
methods: {
|
||||
loadData() {
|
||||
this.loading = true
|
||||
const params = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize }
|
||||
if (this.queryParams.roundId) params.roundId = this.queryParams.roundId
|
||||
if (this.queryParams.boxType) params.boxType = this.queryParams.boxType
|
||||
getRoundList(params).then(res => {
|
||||
this.roundList = (res.data && res.data.rows) || []
|
||||
this.total = (res.data && res.data.total) || 0
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.loadData() },
|
||||
resetQuery() { this.queryParams = { pageNum: 1, pageSize: 10, roundId: null, boxType: null }; this.loadData() }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user