1.修改游戏档位和奖励金额的配置

2.优化T4和T5的奖励权重可以修改
This commit is contained in:
2026-06-01 09:53:24 +08:00
parent 18944f0d48
commit 906539995d
11 changed files with 322 additions and 82 deletions

View File

@@ -25,8 +25,6 @@
"emptyTier": "No data for this tier",
"sumLineDual": "Tier weight sum (clockwise): {cw}; counter-clockwise: {ccw} (each row 110000, ratio draw within tier, sum not limited)",
"sumLineSingle": "Tier weight sum: {sum} (each row 110000, ratio draw within tier, sum not limited)",
"t4t5NoteSingle": "T4 and T5 have a single outcome; no weight configuration.",
"t4t5NoteDual": "T4 and T5 have a single outcome when hit; no weight configuration.",
"colEndIndexId": "End Index (id)",
"colGridNumber": "Points (grid_number)",
"colDicePoints": "Dice Points",
@@ -45,11 +43,11 @@
},
"weightEdit": {
"title": "Dice Reward (dice_reward) Weight Ratio",
"globalTip": "You are editing weights on dice_reward (DiceReward), split by end_index into clockwise and counter-clockwise; the draw uses the set for the current direction."
"globalTip": "You are editing weights on dice_reward (DiceReward), split by end_index into clockwise and counter-clockwise; the draw uses the set for the current direction. T4/T5 support the same multi-point weight ratio as T1T3."
},
"weightRatio": {
"title": "Weight Ratio",
"globalTip": "Configure dice_reward weights: first by direction (clockwise / counter-clockwise), then by tier (T1T5); each row weight 110000, ratio draw within tier.",
"globalTip": "Configure dice_reward weights: first by direction (clockwise / counter-clockwise), then by tier (T1T5); each row weight 110000, ratio draw within tier for dice points (T4/T5 use the same logic as T1T3 and support multiple point weights).",
"tabClockwise": "Clockwise",
"tabCounterclockwise": "Counter-clockwise"
},

View File

@@ -8,6 +8,17 @@
"tabIndex": "Reward Index",
"tabBigwin": "Big Win Weights",
"tipIndex": "Dice points must be between 5 and 30 and unique in this table.",
"tierRecommendRules": "[Settlement vs tier] Settlement < 0 → T4; 0 < Settlement < 1 → T3; 1 < Settlement < 2 → T2; Settlement > 2 → T1; T5 “try again” settlement = 0. Set a unified settlement standard per tier below; values are written on generation and can be edited row by row in the table.",
"tierRecommendRealEv": "Recommended settlement",
"tierRecommendAutoMatch": "Auto-match tier when settlement changes",
"tierRecommendApplyAmount": "Fill recommended amount for rows with tier set",
"tierRecommendApplyAmountOk": "Filled recommended settlement for {n} row(s)",
"tierRecommendNoTierRows": "Set tier T1T5 on table rows first",
"tierRecommendMatchTier": "Match all tiers from settlement",
"tierRecommendMatchTierOk": "Matched tier for {n} row(s) from settlement",
"tierRecommendMatchTierNone": "No rows to match",
"tierRecommendT5UiText": "再来一次",
"tierRecommendT5UiTextEn": "Once again",
"tipBigwin": "Left to right: big-win points (read-only), display text, real EV, remark, weight (0~10000). Points 5 and 30 are fixed at 100%. This tab saves big-win weights only.",
"colId": "Index (id)",
"colDicePoints": "Dice Points",
@@ -54,7 +65,7 @@
"infoNoBigwin": "No BIGWIN rows. Set tier to BIGWIN in the Reward Index tab first.",
"btnRuleGenerate": "Generate by rules",
"ruleGenerateTitle": "Generate reward index by rules",
"ruleGenerateRules": "[Generation logic (same as Create Reward Reference)]\n• 26 cells ordered by id ascending are positions 025; each rows grid_number is 530 and unique.\n• Roll D (530): start at the cell whose grid_number equals D (start_index); clockwise landing = (start position + D) mod 26; counter-clockwise = start D (if negative, +26).\n• Each reference rows “dice points” column is the roll D; tier / real_ev / display text come from the config at the landing id.\n\n[Leopard rolls]\nFor rolls 5, 10, 15, 20, 25, 30, clockwise and counter-clockwise landing tiers must NOT be T4 or T5 (avoid leopard roll + penalty / once again).\n\n[Settlement amount vs tier]\nSettlement < 0 → T4; 0 < Settlement < 100 → T3; 100 < Settlement < 200 → T2; Settlement > 200 → T1; T5 “once again” settlement = 0. You can set a unified settlement standard for each tier below; generated rows write those values into the config, and details can be edited later in the table.\n\n[Inputs in this dialog]\nCount: T1/T4/T5 are fixed; T2 is minimum. Clockwise and counter-clockwise weighted counts (each roll result counts once) must each satisfy the entered values; T1, T4, and T5 are entered separately.\nSettlement standard: all cells in the same tier use the same value. On generation, T1T4 use ui_text / ui_text_en = settlement real_ev; T5 is fixed to \"再来一次\" / \"Once again\". Remarks still distinguish break-even / small win, etc.",
"ruleGenerateRules": "[Generation logic (same as Create Reward Reference)]\n• 26 cells ordered by id ascending are positions 025; each rows grid_number is 530 and unique.\n• Roll D (530): start at the cell whose grid_number equals D (start_index); clockwise landing = (start position + D) mod 26; counter-clockwise = start D (if negative, +26).\n• Each reference rows “dice points” column is the roll D; tier / real_ev / display text come from the config at the landing id.\n\n[Leopard rolls]\nFor rolls 5, 10, 15, 20, 25, 30, clockwise and counter-clockwise landing tiers must NOT be T4 or T5 (avoid leopard roll + penalty / once again).\n\n[Settlement amount vs tier]\nSettlement < 0 → T4; 0 < Settlement < 1 → T3; 1 < Settlement < 2 → T2; Settlement > 2 → T1; T5 “once again” settlement = 0. You can set a unified settlement standard for each tier below; generated rows write those values into the config, and details can be edited later in the table.\n\n[Inputs in this dialog]\nCount: T1/T4/T5 are fixed; T2 is minimum. Clockwise and counter-clockwise weighted counts (each roll result counts once) must each satisfy the entered values; T1, T4, and T5 are entered separately.\nSettlement standard: all cells in the same tier use the same value. On generation, T1T4 use ui_text / ui_text_en = settlement real_ev; T5 is fixed to \"再来一次\" / \"Once again\". Remarks still distinguish break-even / small win, etc.",
"ruleGenT1Row": "T1 (big prize)",
"ruleGenT2Row": "T2 (small win / break-even)",
"ruleGenT3RealEvOnly": "T3 (rake)",
@@ -64,9 +75,9 @@
"ruleGenFixedCount": "Fixed count (CW & CCW)",
"ruleGenRealEvStd": "real_ev standard",
"ruleGenRealEvEditHint": "After saving, you can still edit display text, EN, real_ev and remarks per row in the table above.",
"ruleGenInvalidT1RealEv": "T1 settlement amount must satisfy: value > 200",
"ruleGenInvalidT2RealEv": "T2 settlement amount must satisfy: 100 < value < 200",
"ruleGenInvalidT3RealEv": "T3 settlement amount must satisfy: 0 < value < 100",
"ruleGenInvalidT1RealEv": "T1 settlement amount must satisfy: value > 2",
"ruleGenInvalidT2RealEv": "T2 settlement amount must satisfy: 1 < value < 2",
"ruleGenInvalidT3RealEv": "T3 settlement amount must satisfy: 0 < value < 1",
"ruleGenInvalidT4RealEv": "T4 settlement amount must satisfy: value < 0",
"ruleGenInvalidT5RealEv": "T5 “try again” real_ev must be 0",
"ruleGenT1Min": "T1 fixed count (CW & CCW)",

View File

@@ -25,8 +25,6 @@
"emptyTier": "该档位暂无配置数据",
"sumLineDual": "当前档位权重合计(顺时针):{cw};逆时针:{ccw}(各条 1-10000档位内按权重比抽取和不限制",
"sumLineSingle": "当前档位权重合计:{sum}(各条 1-10000档位内按权重比抽取和不限制",
"t4t5NoteSingle": "T4、T5 仅单一结果,无需配置权重。",
"t4t5NoteDual": "T4、T5 档位抽中时仅有一个结果,无需配置权重。",
"colEndIndexId": "结束索引(id)",
"colGridNumber": "点数(grid_number)",
"colDicePoints": "色子点数",
@@ -45,11 +43,11 @@
},
"weightEdit": {
"title": "奖励对照表dice_reward权重配比",
"globalTip": "编辑的是奖励对照表dice_reward / DiceReward 模型的权重按结束索引end_index区分顺时针与逆时针两套权重抽奖时按当前方向取对应权重。"
"globalTip": "编辑的是奖励对照表dice_reward / DiceReward 模型的权重按结束索引end_index区分顺时针与逆时针两套权重抽奖时按当前方向取对应权重T4/T5 与 T1-T3 相同支持多点数权重配比。"
},
"weightRatio": {
"title": "权重配比",
"globalTip": "配置奖励对照表dice_reward的权重一级按方向顺时针/逆时针二级按档位T1-T5各条权重 1-10000档位内按权重比抽取。",
"globalTip": "配置奖励对照表dice_reward的权重一级按方向顺时针/逆时针二级按档位T1-T5各条权重 1-10000档位内按权重比抽取骰子点数T4/T5 与 T1-T3 逻辑相同,可配置多个点数的权重配比)。",
"tabClockwise": "顺时针",
"tabCounterclockwise": "逆时针"
},

View File

@@ -8,6 +8,17 @@
"tabIndex": "奖励索引",
"tabBigwin": "大奖权重",
"tipIndex": "色子点数须在 530 之间且本表内不重复。",
"tierRecommendRules": "【结算金额与档位】结算金额 < 0 → T40 < 结算金额 < 1 → T31 < 结算金额 < 2 → T22 < 结算金额 → T1T5「再来一次」结算金额 = 0。下方可为各档位填写统一的结算金额标准生成时写入配置细则可稍后在表格中再改。",
"tierRecommendRealEv": "推荐结算金额",
"tierRecommendAutoMatch": "修改结算金额时自动匹配档位",
"tierRecommendApplyAmount": "将推荐金额填入已选档位的行",
"tierRecommendApplyAmountOk": "已为 {n} 行填入推荐结算金额",
"tierRecommendNoTierRows": "请先在表格中为行选择 T1T5 档位",
"tierRecommendMatchTier": "按结算金额匹配全部档位",
"tierRecommendMatchTierOk": "已根据结算金额为 {n} 行匹配档位",
"tierRecommendMatchTierNone": "没有可匹配档位的行",
"tierRecommendT5UiText": "再来一次",
"tierRecommendT5UiTextEn": "Once again",
"tipBigwin": "从左至右:中大奖点数(不可改)、显示信息、实际中奖、备注、权重(0~10000)。点数 5、30 权重固定 100%。本表单独立提交,仅提交大奖权重。",
"colId": "索引(id)",
"colDicePoints": "色子点数",
@@ -54,7 +65,7 @@
"infoNoBigwin": "暂无 BIGWIN 档位配置,请先在「奖励索引」中设置 tier 为 BIGWIN",
"btnRuleGenerate": "按规则生成",
"ruleGenerateTitle": "按规则生成奖励索引",
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 025每条配置的 grid_number 为 530 且不重复。\n• 摇取点数 D530起点为「grid_number=D」所在格位的 id即 start_index顺时针落点位置 = (起点位置 + D) mod 26逆时针落点 = 起点位置 D若小于 0 则 +26。\n• 对照表每条记录的「色子点数」列为摇取点数 D档位、真实结算、显示文案取自落点格位对应 id 的配置。\n\n【豹子摇取点数】\n摇取点数为 5、10、15、20、25、30 时,其顺/逆时针落点档位不能为 T4、T5避免对照表上出现豹子点数 + 惩罚/再来一次)。\n\n【结算金额 与 档位】\n结算金额 < 0 → T40 < 结算金额 < 100 → T3100 < 结算金额 < 200 → T2200 < 结算金额 → T1T5「再来一次」结算金额=0。下方可为各档位填写统一的 结算金额 标准,生成时写入配置;细则可稍后在表格中再改。\n\n【本弹窗输入】\n条数T1/T4/T5「固定」T2「不少于」——顺时针与逆时针的加权条数每条摇取结果计一次须分别满足所填数值T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1T4 的 显示文本 / 显示文本(英文) = 结算金额T5 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 025每条配置的 grid_number 为 530 且不重复。\n• 摇取点数 D530起点为「grid_number=D」所在格位的 id即 start_index顺时针落点位置 = (起点位置 + D) mod 26逆时针落点 = 起点位置 D若小于 0 则 +26。\n• 对照表每条记录的「色子点数」列为摇取点数 D档位、真实结算、显示文案取自落点格位对应 id 的配置。\n\n【豹子摇取点数】\n摇取点数为 5、10、15、20、25、30 时,其顺/逆时针落点档位不能为 T4、T5避免对照表上出现豹子点数 + 惩罚/再来一次)。\n\n【结算金额 与 档位】\n结算金额 < 0 → T40 < 结算金额 < 1 → T31 < 结算金额 < 2 → T22 < 结算金额 → T1T5「再来一次」结算金额=0。下方可为各档位填写统一的 结算金额 标准,生成时写入配置;细则可稍后在表格中再改。\n\n【本弹窗输入】\n条数T1/T4/T5「固定」T2「不少于」——顺时针与逆时针的加权条数每条摇取结果计一次须分别满足所填数值T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1T4 的 显示文本 / 显示文本(英文) = 结算金额T5 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
"ruleGenT1Row": "T1 大奖",
"ruleGenT2Row": "T2 小赚/回本",
"ruleGenT3RealEvOnly": "T3 抽水",
@@ -64,9 +75,9 @@
"ruleGenFixedCount": "固定条数(顺/逆)",
"ruleGenRealEvStd": "结算金额",
"ruleGenRealEvEditHint": "生成并保存后,仍可在本页表格中逐条修改显示文案、英文、真实结算与备注。",
"ruleGenInvalidT1RealEv": "T1 的 结算金额 满足200 < 值",
"ruleGenInvalidT2RealEv": "T2 的 结算金额 满足100 < 值 < 200",
"ruleGenInvalidT3RealEv": "T3 的 结算金额 满足0 < 值 < 100",
"ruleGenInvalidT1RealEv": "T1 的 结算金额 满足2 < 值",
"ruleGenInvalidT2RealEv": "T2 的 结算金额 满足1 < 值 < 2",
"ruleGenInvalidT3RealEv": "T3 的 结算金额 满足0 < 值 < 1",
"ruleGenInvalidT4RealEv": "T4 的 结算金额 满足:值 < 0",
"ruleGenInvalidT5RealEv": "T5「再来一次」的 结算金额 须为 0",
"ruleGenT1Min": "T1 固定条数(顺/逆)",

View File

@@ -76,6 +76,27 @@ export default {
})
},
/**
* 按规则生成并保存奖励索引(需 dice:reward_config:index:tierRecommend 权限)
*/
generateIndexByRules(
items: Array<{
id: number
grid_number?: number
ui_text?: string
ui_text_en?: string
real_ev?: number
tier?: string
remark?: string
}>,
extra?: Record<string, any>
) {
return request.post<any>({
url: '/core/dice/reward_config/DiceRewardConfig/generateIndexByRules',
data: { items, ...(extra || {}) }
})
},
/**
* T1-T5、BIGWIN 权重配比:按档位分组获取配置列表
*/

View File

@@ -15,7 +15,7 @@
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<div class="chart-wrap">
<div class="chart-row">
<ArtBarChart
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
@@ -31,7 +31,7 @@
/>
</div>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
<div class="weight-sum">
{{
$t('page.weightShared.sumLineDual', {
cw: getTierSum(t, 'clockwise'),
@@ -39,7 +39,6 @@
})
}}
</div>
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column
:label="$t('page.weightShared.colEndIndexId')"
@@ -89,7 +88,6 @@
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) =>
@@ -106,7 +104,7 @@
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
:disabled="getItemWeight(row, 'clockwise') <= 1"
@click="
setItemWeightByRow(
t,
@@ -122,7 +120,6 @@
:min="1"
:max="10000"
:step="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@@ -140,7 +137,7 @@
type="primary"
link
:disabled="
isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000
getItemWeight(row, 'clockwise') >= 10000
"
@click="
setItemWeightByRow(
@@ -166,7 +163,6 @@
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) =>
@@ -184,7 +180,7 @@
type="primary"
link
:disabled="
isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1
getItemWeight(row, 'counterclockwise') <= 1
"
@click="
setItemWeightByRow(
@@ -201,7 +197,6 @@
:min="1"
:max="10000"
:step="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@@ -219,7 +214,6 @@
type="primary"
link
:disabled="
isWeightDisabled(row, t) ||
getItemWeight(row, 'counterclockwise') >= 10000
"
@click="
@@ -361,11 +355,6 @@
else row[key] = v
}
function isWeightDisabled(row: WeightRow, tier: string): boolean {
if (tier === 'T4' || tier === 'T5') return true
return false
}
function normalizeWeightValue(v: unknown): number {
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
if (Number.isNaN(num)) return 1
@@ -465,8 +454,8 @@
const items: Array<{ id: number; weight: number }> = []
for (const t of TIER_KEYS) {
for (const row of getTierItems(t)) {
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
const w0 = getItemWeight(row, 'clockwise')
const w1 = getItemWeight(row, 'counterclockwise')
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
@@ -530,9 +519,6 @@
margin-bottom: 12px;
font-size: 13px;
}
.weight-sum-t4t5 {
color: var(--el-text-color-secondary);
}
.global-tip {
margin-bottom: 12px;
padding: 10px 12px;

View File

@@ -18,7 +18,7 @@
<el-tab-pane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<div class="chart-wrap">
<ArtBarChart
:key="'cw-' + activeDirection + '-' + t"
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
@@ -27,12 +27,11 @@
height="180px"
/>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
<div class="weight-sum">
{{
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
}}
</div>
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column
:label="$t('page.weightShared.colGridNumber')"
@@ -87,7 +86,6 @@
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) =>
@@ -103,9 +101,7 @@
<el-button
type="primary"
link
:disabled="
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
"
:disabled="getItemWeightForCurrentDirection(row) <= 1"
@click="
setItemWeightForCurrentDirection(
t,
@@ -120,7 +116,6 @@
:min="1"
:max="10000"
:step="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@@ -136,10 +131,7 @@
<el-button
type="primary"
link
:disabled="
isWeightDisabled(row, t) ||
getItemWeightForCurrentDirection(row) >= 10000
"
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
@click="
setItemWeightForCurrentDirection(
t,
@@ -163,7 +155,7 @@
<el-tab-pane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
<template v-else>
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
<div class="chart-wrap">
<ArtBarChart
:key="'ccw-' + activeDirection + '-' + t"
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
@@ -172,12 +164,11 @@
height="180px"
/>
</div>
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
<div class="weight-sum">
{{
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
}}
</div>
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
<el-table-column
:label="$t('page.weightShared.colGridNumber')"
@@ -228,7 +219,6 @@
:max="10000"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) =>
@@ -244,9 +234,7 @@
<el-button
type="primary"
link
:disabled="
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
"
:disabled="getItemWeightForCurrentDirection(row) <= 1"
@click="
setItemWeightForCurrentDirection(
t,
@@ -261,7 +249,6 @@
:min="1"
:max="10000"
:step="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@@ -277,10 +264,7 @@
<el-button
type="primary"
link
:disabled="
isWeightDisabled(row, t) ||
getItemWeightForCurrentDirection(row) >= 10000
"
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
@click="
setItemWeightForCurrentDirection(
t,
@@ -433,11 +417,6 @@
grouped.value[tier][dir] = [...list]
}
function isWeightDisabled(row: WeightRow, tier: string): boolean {
if (tier === 'T4' || tier === 'T5') return true
return false
}
function normalizeWeightValue(v: unknown): number {
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
if (Number.isNaN(num)) return 1
@@ -506,7 +485,7 @@
for (const row of list) {
const rid = row.reward_id != null ? Number(row.reward_id) : 0
if (rid <= 0) continue
const w = isWeightDisabled(row, t) ? 10000 : toWeightPrecision(row.weight ?? 1)
const w = toWeightPrecision(row.weight ?? 1)
items.push({ id: rid, reward_id: rid, weight: w })
}
}
@@ -567,9 +546,6 @@
margin-bottom: 12px;
font-size: 13px;
}
.weight-sum-t4t5 {
color: var(--el-text-color-secondary);
}
.global-tip {
margin-bottom: 12px;
padding: 10px 12px;

View File

@@ -21,9 +21,38 @@
<ElTabPane :label="$t('page.configPage.tabIndex')" name="index">
<div class="tab-panel">
<div class="panel-tip">{{ $t('page.configPage.tipIndex') }}</div>
<div class="index-toolbar">
<div v-if="canTierRecommend" class="tier-recommend-panel">
<div class="tier-recommend-rules">{{ $t('page.configPage.tierRecommendRules') }}</div>
<div class="tier-recommend-grid">
<div v-for="tk in TIER_RECOMMEND_KEYS" :key="tk" class="tier-recommend-cell">
<span class="tier-recommend-label">{{ tk }}</span>
<span class="tier-recommend-hint">{{ $t('page.configPage.tierRecommendRealEv') }}</span>
<ElInputNumber
v-model="tierRecommend[tk]"
:disabled="tk === 'T5'"
:step="0.1"
:precision="2"
controls-position="right"
size="small"
class="tier-recommend-input"
/>
</div>
</div>
<div class="tier-recommend-actions">
<ElCheckbox v-model="autoMatchTierOnRealEv">{{
$t('page.configPage.tierRecommendAutoMatch')
}}</ElCheckbox>
<ElButton size="small" @click="handleApplyRecommendRealEv" v-ripple>{{
$t('page.configPage.tierRecommendApplyAmount')
}}</ElButton>
<ElButton size="small" type="primary" plain @click="handleMatchAllTiersFromRealEv" v-ripple>{{
$t('page.configPage.tierRecommendMatchTier')
}}</ElButton>
</div>
</div>
<div v-if="canTierRecommend" class="index-toolbar">
<ElButton
v-permission="'dice:reward_config:index:batchUpdate'"
v-permission="PERM_TIER_RECOMMEND"
type="default"
@click="openRuleGenerateDialog"
v-ripple
@@ -286,6 +315,7 @@
</ElCard>
<ElDialog
v-if="canTierRecommend"
v-model="ruleGenerateDialogVisible"
:title="$t('page.configPage.ruleGenerateTitle')"
:width="ruleGenDialogWidth"
@@ -456,17 +486,24 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import api from '../../api/reward_config/index'
import { checkAuth } from '@/utils/tool'
import {
buildRowsFromTiers,
computeBoardFrequencies,
DEFAULT_TIER_REAL_EV_STANDARDS,
generateTiers,
inferTierFromRealEv,
summarizeCounts,
type TierRealEvStandards,
validateTierRealEvStandards
} from '../utils/generateIndexByRules'
const { t } = useI18n()
/** 档位结算推荐配置T1-T5 推荐金额栏、按规则生成/导入) */
const PERM_TIER_RECOMMEND = 'dice:reward_config:index:tierRecommend'
const canTierRecommend = computed(() => checkAuth(PERM_TIER_RECOMMEND))
const { width: viewportWidth } = useWindowSize()
/** 窄屏:单列、标签置顶、全屏弹窗 */
const isRuleGenMobile = computed(() => viewportWidth.value < 640)
@@ -516,12 +553,57 @@
const ruleGenT2Min = ref(5)
const ruleGenT4Fixed = ref(1)
const ruleGenT5Fixed = ref(1)
const TIER_RECOMMEND_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
type TierRecommendKey = (typeof TIER_RECOMMEND_KEYS)[number]
const tierRecommend = reactive<TierRealEvStandards>({
T1: DEFAULT_TIER_REAL_EV_STANDARDS.T1,
T2: DEFAULT_TIER_REAL_EV_STANDARDS.T2,
T3: DEFAULT_TIER_REAL_EV_STANDARDS.T3,
T4: DEFAULT_TIER_REAL_EV_STANDARDS.T4,
T5: DEFAULT_TIER_REAL_EV_STANDARDS.T5
})
const autoMatchTierOnRealEv = ref(false)
const ruleGenT1RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T1)
const ruleGenT2RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T2)
const ruleGenT3RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T3)
const ruleGenT4RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T4)
const ruleGenT5RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T5)
function syncRuleGenFromTierRecommend() {
ruleGenT1RealEv.value = tierRecommend.T1
ruleGenT2RealEv.value = tierRecommend.T2
ruleGenT3RealEv.value = tierRecommend.T3
ruleGenT4RealEv.value = tierRecommend.T4
ruleGenT5RealEv.value = tierRecommend.T5
}
function syncTierRecommendFromRuleGen() {
tierRecommend.T1 = Number(ruleGenT1RealEv.value)
tierRecommend.T2 = Number(ruleGenT2RealEv.value)
tierRecommend.T3 = Number(ruleGenT3RealEv.value)
tierRecommend.T4 = Number(ruleGenT4RealEv.value)
tierRecommend.T5 = Number(ruleGenT5RealEv.value)
}
function applyRealEvDisplay(row: IndexRow, n: number) {
const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
row.ui_text = text
row.ui_text_en = text
}
function applyRecommendRealEvToRow(row: IndexRow, tier: TierRecommendKey) {
const ev = tierRecommend[tier]
row.real_ev = ev
if (tier === 'T5') {
row.ui_text = t('page.configPage.tierRecommendT5UiText')
row.ui_text_en = t('page.configPage.tierRecommendT5UiTextEn')
} else {
applyRealEvDisplay(row, ev)
}
}
/** 奖励索引 id 与后端 DiceRewardConfigLogic 一致025 */
const REWARD_INDEX_MIN = 0
const REWARD_INDEX_MAX = 25
@@ -577,9 +659,55 @@
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
? row.real_ev
: Number(row.real_ev)
const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
row.ui_text = text
row.ui_text_en = text
applyRealEvDisplay(row, n)
if (canTierRecommend.value && autoMatchTierOnRealEv.value) {
const tier = inferTierFromRealEv(n)
if (tier !== '') {
row.tier = tier
}
}
}
function handleApplyRecommendRealEv() {
if (!canTierRecommend.value) {
return
}
let count = 0
for (const row of indexRowsExcludeBigwin.value) {
const tier = row.tier
if (tier === 'T1' || tier === 'T2' || tier === 'T3' || tier === 'T4' || tier === 'T5') {
applyRecommendRealEvToRow(row, tier)
count++
}
}
if (count === 0) {
ElMessage.info(t('page.configPage.tierRecommendNoTierRows'))
return
}
ElMessage.success(t('page.configPage.tierRecommendApplyAmountOk', { n: count }))
}
function handleMatchAllTiersFromRealEv() {
if (!canTierRecommend.value) {
return
}
let count = 0
for (const row of indexRowsExcludeBigwin.value) {
const n =
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
? row.real_ev
: Number(row.real_ev)
const tier = inferTierFromRealEv(n)
if (tier !== '') {
row.tier = tier
count++
}
}
if (count === 0) {
ElMessage.info(t('page.configPage.tierRecommendMatchTierNone'))
return
}
ElMessage.success(t('page.configPage.tierRecommendMatchTierOk', { n: count }))
}
function formatMoney2(val: unknown): string {
@@ -745,6 +873,10 @@
}
function openRuleGenerateDialog() {
if (!canTierRecommend.value) {
return
}
syncRuleGenFromTierRecommend()
ruleGenerateDialogVisible.value = true
}
@@ -781,6 +913,9 @@
}
async function handleRuleGenerateApply() {
if (!canTierRecommend.value) {
return
}
const grids = extractGrids26()
if (grids === null) {
ElMessage.warning(t('page.configPage.ruleGenNeedFullGrid'))
@@ -790,12 +925,13 @@
const t2 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT2Min.value))))
const x4 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT4Fixed.value))))
const x5 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT5Fixed.value))))
syncTierRecommendFromRuleGen()
const standards = {
T1: Number(ruleGenT1RealEv.value),
T2: Number(ruleGenT2RealEv.value),
T3: Number(ruleGenT3RealEv.value),
T4: Number(ruleGenT4RealEv.value),
T5: Number(ruleGenT5RealEv.value)
T1: Number(tierRecommend.T1),
T2: Number(tierRecommend.T2),
T3: Number(tierRecommend.T3),
T4: Number(tierRecommend.T4),
T5: Number(tierRecommend.T5)
}
const invalidKey = validateTierRealEvStandards(standards)
if (invalidKey !== null) {
@@ -850,7 +986,7 @@
remark: r.remark
})
)
await api.batchUpdate(indexPayload, { dept_id: filterDeptId.value })
await api.generateIndexByRules(indexPayload, { dept_id: filterDeptId.value })
ElMessage.success(
t('page.configPage.ruleGenSuccess', {
cwT1: sc.cw.T1,
@@ -1058,6 +1194,57 @@
margin-bottom: 12px;
line-height: 1.5;
}
.tier-recommend-panel {
margin-bottom: 12px;
padding: 12px 14px;
background: var(--el-fill-color-light);
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
flex-shrink: 0;
}
.tier-recommend-rules {
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.6;
margin-bottom: 12px;
}
.tier-recommend-grid {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
margin-bottom: 12px;
}
.tier-recommend-cell {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
min-width: 140px;
flex: 1 1 160px;
}
.tier-recommend-label {
font-weight: 600;
font-size: 13px;
color: var(--el-text-color-primary);
min-width: 28px;
}
.tier-recommend-hint {
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.tier-recommend-input {
width: 120px;
flex: 1;
min-width: 100px;
max-width: 160px;
}
.tier-recommend-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
}
.index-toolbar {
display: flex;
align-items: center;

View File

@@ -53,7 +53,7 @@ export interface TierRealEvStandards {
T5: number
}
/** 默认标准(与规则弹窗说明一致 */
/** 默认推荐结算金额(满足档位区间规则,可在页面修改 */
export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
T1: 3,
T2: 1.5,
@@ -62,6 +62,38 @@ export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
T5: 0
}
/**
* 按结算金额推断档位(与奖励配置页规则说明一致)
* 结算金额 < 0 → T40 < 结算金额 < 1 → T31 < 结算金额 < 2 → T22 < 结算金额 → T1T5 结算金额 = 0
*/
export function inferTierFromRealEv(realEv: number): IndexTier | '' {
if (!Number.isFinite(realEv)) {
return ''
}
if (realEv === 0) {
return 'T5'
}
if (realEv < 0) {
return 'T4'
}
if (realEv > 2) {
return 'T1'
}
if (realEv > 1 && realEv < 2) {
return 'T2'
}
if (realEv > 0 && realEv < 1) {
return 'T3'
}
if (realEv === 1) {
return 'T2'
}
if (realEv === 2) {
return 'T1'
}
return ''
}
/**
* 校验档位与 real_ev 区间是否一致;通过返回 null否则返回 i18n 键名(不含 page.configPage. 前缀)
*/

View File

@@ -127,6 +127,25 @@ class DiceRewardConfigController extends BaseController
*/
#[Permission('修改奖励索引', 'dice:reward_config:index:batchUpdate')]
public function batchUpdate(Request $request): Response
{
return $this->doBatchUpdateIndex($request);
}
/**
* 按规则生成并保存奖励索引(需档位结算推荐配置权限,与 batchUpdate 写入逻辑相同)
*
* @param Request $request items: [{ id, grid_number?, ui_text?, ui_text_en?, real_ev?, tier?, remark? }, ...]
*/
#[Permission('档位结算推荐配置', 'dice:reward_config:index:tierRecommend')]
public function generateIndexByRules(Request $request): Response
{
return $this->doBatchUpdateIndex($request);
}
/**
* @return Response
*/
private function doBatchUpdateIndex(Request $request): Response
{
$items = $request->post('items', []);
if (! is_array($items)) {

View File

@@ -119,6 +119,7 @@ Route::group('/core', function () {
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
Route::post('/dice/reward_config/DiceRewardConfig/saveBigwinWeightsByGrid', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'saveBigwinWeightsByGrid']);
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdate', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdate']);
Route::post('/dice/reward_config/DiceRewardConfig/generateIndexByRules', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'generateIndexByRules']);
Route::post('/dice/reward_config/DiceRewardConfig/createRewardReference', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReference']);
Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']);
fastRoute('dice/game/DiceGame', \app\dice\controller\game\DiceGameController::class);