1.优化奖励配置设置
This commit is contained in:
@@ -8,17 +8,18 @@
|
||||
"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.",
|
||||
"tierRecommendRules": "[Settlement vs tier] T1 (big prize): >2; T2 (small win): 2≥amount>1; T3 (rake): 1≥amount>0; T4 (try again): =0; T5 (penalty): 0>amount. Set recommended settlement per tier below. The Tier column is auto-calculated from settlement and cannot be edited manually.",
|
||||
"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 T1–T5 on table rows first",
|
||||
"tierRecommendNoTierRows": "No rows with a tier inferable from settlement",
|
||||
"tierRecommendMatchTier": "Match all tiers from settlement",
|
||||
"tierRecommendMatchTierOk": "Matched tier for {n} row(s) from settlement",
|
||||
"tierRecommendMatchTierNone": "No rows to match",
|
||||
"tierRecommendT5UiText": "再来一次",
|
||||
"tierRecommendT5UiTextEn": "Once again",
|
||||
"tierRecommendT4UiText": "再来一次",
|
||||
"tierRecommendT4UiTextEn": "Once again",
|
||||
"colTierAutoHint": "Auto-matched from settlement",
|
||||
"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",
|
||||
@@ -47,6 +48,18 @@
|
||||
"confirmCreateRefMsg": "Create reward reference by rule: start_index is the id of the cell for grid_number in reward config; clockwise end_index=(start_index+roll)%26; counter-clockwise end_index=start_index-roll if >=0 else 26+start_index-roll. Existing data will be cleared, then 26 points (5–30) for both directions will be generated. Continue?",
|
||||
"confirmCreateRefOk": "Create",
|
||||
"confirmCreateRefCancel": "Cancel",
|
||||
"createRefPreviewTitle": "Create Reward Reference Preview",
|
||||
"createRefPreviewClockwise": "Clockwise",
|
||||
"createRefPreviewCounterclockwise": "Counter-clockwise",
|
||||
"createRefPreviewTipUnchanged": "Dice points mapping is unchanged: weights in the preview are reused from current dice_reward; importing will not override existing weights.",
|
||||
"createRefPreviewTipChanged": "Dice points mapping has changed: preview weights use the default value (1). After importing, adjust weights in the Dice Reward page if needed.",
|
||||
"createRefPreviewSkipped": "{n} dice point(s) are missing in the reward index and were skipped (please complete all 26 points from 5 to 30).",
|
||||
"createRefPreviewRefresh": "Refresh preview",
|
||||
"createRefPreviewImport": "Import",
|
||||
"createRefPreviewImportOk": "Imported reward reference",
|
||||
"createRefPreviewImportNoop": "Mapping unchanged, nothing to import (existing weights kept)",
|
||||
"createRefPreviewDiff": "Diff (old → new)",
|
||||
"createRefPreviewNoDiff": "No change",
|
||||
"createRefSuccess": "Created for 26 dice points (5–30), clockwise + counter-clockwise: clockwise added {cwNew}, counter-clockwise added {ccwNew}; clockwise updated {cwUp}, counter-clockwise updated {ccwUp}{skippedPart}",
|
||||
"createRefSuccessSkipped": "; {n} point(s) used fallback start index",
|
||||
"createRefSuccessSimple": "Created successfully",
|
||||
@@ -65,21 +78,21 @@
|
||||
"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 0–25; each row’s grid_number is 5–30 and unique.\n• Roll D (5–30): 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 row’s “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, T1–T4 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 0–25; each row’s grid_number is 5–30 and unique.\n• Roll D (5–30): 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 row’s “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 + try again / penalty).\n\n[Settlement vs tier]\nT1: >2; T2: 2≥amount>1; T3: 1≥amount>0; T4: =0; T5: 0>amount. Set recommended settlement per tier below.\n\n[Inputs in this dialog]\nCount: T1/T4/T5 are fixed; T2 is minimum. Clockwise and counter-clockwise weighted counts must each satisfy the entered values.\nSettlement standard: same tier uses the same value. T1–T3 and T5 use ui_text = settlement; T4 is fixed to \"再来一次\" / \"Once again\".",
|
||||
"ruleGenT1Row": "T1 (big prize)",
|
||||
"ruleGenT2Row": "T2 (small win / break-even)",
|
||||
"ruleGenT3RealEvOnly": "T3 (rake)",
|
||||
"ruleGenT4Row": "T4 (penalty)",
|
||||
"ruleGenT5Row": "T5 (try again)",
|
||||
"ruleGenT4Row": "T4 (try again)",
|
||||
"ruleGenT5Row": "T5 (penalty)",
|
||||
"ruleGenMinCount": "Min count",
|
||||
"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 > 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",
|
||||
"ruleGenInvalidT1RealEv": "T1 (big prize) settlement must satisfy: value > 2",
|
||||
"ruleGenInvalidT2RealEv": "T2 (small win) settlement must satisfy: 1 < value ≤ 2",
|
||||
"ruleGenInvalidT3RealEv": "T3 (rake) settlement must satisfy: 0 < value ≤ 1",
|
||||
"ruleGenInvalidT4RealEv": "T4 (try again) settlement must be 0",
|
||||
"ruleGenInvalidT5RealEv": "T5 (penalty) settlement must satisfy: value < 0",
|
||||
"ruleGenT1Min": "T1 fixed count (CW & CCW)",
|
||||
"ruleGenT2Min": "T2 min (CW & CCW)",
|
||||
"ruleGenT4Max": "T4 fixed count (CW & CCW)",
|
||||
|
||||
@@ -8,17 +8,18 @@
|
||||
"tabIndex": "奖励索引",
|
||||
"tabBigwin": "大奖权重",
|
||||
"tipIndex": "色子点数须在 5~30 之间且本表内不重复。",
|
||||
"tierRecommendRules": "【结算金额与档位】结算金额 < 0 → T4;0 < 结算金额 < 1 → T3;1 < 结算金额 < 2 → T2;2 < 结算金额 → T1;T5「再来一次」结算金额 = 0。下方可为各档位填写统一的结算金额标准,生成时写入配置;细则可稍后在表格中再改。",
|
||||
"tierRecommendRules": "【结算金额与档位】【大奖】T1:结算金额>2;【小赚】T2:2>=结算金额>1;【抽水】T3:1>=结算金额>0;【再来一次】T4:结算金额=0;【惩罚】T5:0>结算金额。下方可为各档位填写推荐结算金额;表格中「所属档位」随结算金额自动计算,不可手动修改。",
|
||||
"tierRecommendRealEv": "推荐结算金额",
|
||||
"tierRecommendAutoMatch": "修改结算金额时自动匹配档位",
|
||||
"tierRecommendApplyAmount": "将推荐金额填入已选档位的行",
|
||||
"tierRecommendApplyAmountOk": "已为 {n} 行填入推荐结算金额",
|
||||
"tierRecommendNoTierRows": "请先在表格中为行选择 T1~T5 档位",
|
||||
"tierRecommendNoTierRows": "没有可根据结算金额推断档位的行",
|
||||
"tierRecommendMatchTier": "按结算金额匹配全部档位",
|
||||
"tierRecommendMatchTierOk": "已根据结算金额为 {n} 行匹配档位",
|
||||
"tierRecommendMatchTierNone": "没有可匹配档位的行",
|
||||
"tierRecommendT5UiText": "再来一次",
|
||||
"tierRecommendT5UiTextEn": "Once again",
|
||||
"tierRecommendT4UiText": "再来一次",
|
||||
"tierRecommendT4UiTextEn": "Once again",
|
||||
"colTierAutoHint": "根据结算金额自动匹配",
|
||||
"tipBigwin": "从左至右:中大奖点数(不可改)、显示信息、实际中奖、备注、权重(0~10000)。点数 5、30 权重固定 100%。本表单独立提交,仅提交大奖权重。",
|
||||
"colId": "索引(id)",
|
||||
"colDicePoints": "色子点数",
|
||||
@@ -47,6 +48,18 @@
|
||||
"confirmCreateRefMsg": "按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?",
|
||||
"confirmCreateRefOk": "确定创建",
|
||||
"confirmCreateRefCancel": "取消",
|
||||
"createRefPreviewTitle": "创建奖励对照预览",
|
||||
"createRefPreviewClockwise": "顺时针",
|
||||
"createRefPreviewCounterclockwise": "逆时针",
|
||||
"createRefPreviewTipUnchanged": "检测到色子点数映射未变化:预览中权重将复用当前奖励对照表(dice_reward)的权重;导入时不会覆盖现有权重。",
|
||||
"createRefPreviewTipChanged": "检测到色子点数映射已变化:预览中权重将使用默认值(1);确认导入后可再到「奖励对照」页面调整权重。",
|
||||
"createRefPreviewSkipped": "有 {n} 个点数在当前奖励索引中缺失,已跳过生成(请先补齐 5~30 共 26 个点数)。",
|
||||
"createRefPreviewRefresh": "刷新预览",
|
||||
"createRefPreviewImport": "确认导入",
|
||||
"createRefPreviewImportOk": "已导入奖励对照表",
|
||||
"createRefPreviewImportNoop": "色子点数映射未变化,无需导入(已保留现有权重)",
|
||||
"createRefPreviewDiff": "差异(旧 → 新)",
|
||||
"createRefPreviewNoDiff": "无变化",
|
||||
"createRefSuccess": "已按 5-30 共 26 个点数、顺时针+逆时针创建:顺时针新增 {cwNew} 条、逆时针新增 {ccwNew} 条;顺时针更新 {cwUp} 条、逆时针更新 {ccwUp} 条{skippedPart}",
|
||||
"createRefSuccessSkipped": ";{n} 个点数使用兜底起始索引",
|
||||
"createRefSuccessSimple": "创建成功",
|
||||
@@ -65,21 +78,21 @@
|
||||
"infoNoBigwin": "暂无 BIGWIN 档位配置,请先在「奖励索引」中设置 tier 为 BIGWIN",
|
||||
"btnRuleGenerate": "按规则生成",
|
||||
"ruleGenerateTitle": "按规则生成奖励索引",
|
||||
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 0~25;每条配置的 grid_number 为 5~30 且不重复。\n• 摇取点数 D(5~30):起点为「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 → T4;0 < 结算金额 < 1 → T3;1 < 结算金额 < 2 → T2;2 < 结算金额 → T1;T5「再来一次」结算金额=0。下方可为各档位填写统一的 结算金额 标准,生成时写入配置;细则可稍后在表格中再改。\n\n【本弹窗输入】\n条数:T1/T4/T5「固定」;T2「不少于」——顺时针与逆时针的加权条数(每条摇取结果计一次)须分别满足所填数值;T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1~T4 的 显示文本 / 显示文本(英文) = 结算金额;T5 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
|
||||
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 0~25;每条配置的 grid_number 为 5~30 且不重复。\n• 摇取点数 D(5~30):起点为「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【大奖】T1:>2;【小赚】T2:2>=金额>1;【抽水】T3:1>=金额>0;【再来一次】T4:=0;【惩罚】T5:0>金额。下方可为各档位填写推荐结算金额标准,生成时写入配置。\n\n【本弹窗输入】\n条数:T1/T4/T5「固定」;T2「不少于」——顺时针与逆时针的加权条数(每条摇取结果计一次)须分别满足所填数值;T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1~T3、T5 的显示文本 = 结算金额;T4 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
|
||||
"ruleGenT1Row": "T1 大奖",
|
||||
"ruleGenT2Row": "T2 小赚/回本",
|
||||
"ruleGenT3RealEvOnly": "T3 抽水",
|
||||
"ruleGenT4Row": "T4 惩罚",
|
||||
"ruleGenT5Row": "T5 再来一次",
|
||||
"ruleGenT4Row": "T4 再来一次",
|
||||
"ruleGenT5Row": "T5 惩罚",
|
||||
"ruleGenMinCount": "最少条数",
|
||||
"ruleGenFixedCount": "固定条数(顺/逆)",
|
||||
"ruleGenRealEvStd": "结算金额",
|
||||
"ruleGenRealEvEditHint": "生成并保存后,仍可在本页表格中逐条修改显示文案、英文、真实结算与备注。",
|
||||
"ruleGenInvalidT1RealEv": "T1 的 结算金额 满足:2 < 值",
|
||||
"ruleGenInvalidT2RealEv": "T2 的 结算金额 满足:1 < 值 < 2",
|
||||
"ruleGenInvalidT3RealEv": "T3 的 结算金额 满足:0 < 值 < 1",
|
||||
"ruleGenInvalidT4RealEv": "T4 的 结算金额 满足:值 < 0",
|
||||
"ruleGenInvalidT5RealEv": "T5「再来一次」的 结算金额 须为 0",
|
||||
"ruleGenInvalidT1RealEv": "T1(大奖)结算金额须满足:值 > 2",
|
||||
"ruleGenInvalidT2RealEv": "T2(小赚)结算金额须满足:1 < 值 ≤ 2",
|
||||
"ruleGenInvalidT3RealEv": "T3(抽水)结算金额须满足:0 < 值 ≤ 1",
|
||||
"ruleGenInvalidT4RealEv": "T4(再来一次)结算金额须为 0",
|
||||
"ruleGenInvalidT5RealEv": "T5(惩罚)结算金额须满足:值 < 0",
|
||||
"ruleGenT1Min": "T1 固定条数(顺/逆)",
|
||||
"ruleGenT2Min": "T2 最少条数(顺/逆)",
|
||||
"ruleGenT4Max": "T4 固定条数(顺/逆)",
|
||||
|
||||
@@ -147,5 +147,15 @@ export default {
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference',
|
||||
data: params || {}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建奖励对照(预览):不写库,返回将要生成的对照与权重预览
|
||||
*/
|
||||
createRewardReferencePreview(params?: Record<string, any>) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReferencePreview',
|
||||
data: params || {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<span class="tier-recommend-hint">{{ $t('page.configPage.tierRecommendRealEv') }}</span>
|
||||
<ElInputNumber
|
||||
v-model="tierRecommend[tk]"
|
||||
:disabled="tk === 'T5'"
|
||||
:disabled="tk === 'T4'"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
@@ -131,7 +131,8 @@
|
||||
@change="handleRealEvChange(row)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
class="full-width"
|
||||
/>
|
||||
</template>
|
||||
@@ -147,19 +148,7 @@
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('page.configPage.colTier')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElSelect
|
||||
v-model="row.tier"
|
||||
:placeholder="$t('page.configPage.placeholderTierSelect')"
|
||||
clearable
|
||||
size="small"
|
||||
class="full-width"
|
||||
>
|
||||
<ElOption label="T1" value="T1" />
|
||||
<ElOption label="T2" value="T2" />
|
||||
<ElOption label="T3" value="T3" />
|
||||
<ElOption label="T4" value="T4" />
|
||||
<ElOption label="T5" value="T5" />
|
||||
</ElSelect>
|
||||
<span class="tier-readonly">{{ displayRowTier(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
@@ -246,7 +235,8 @@
|
||||
@change="handleRealEvChange(row)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
class="full-width"
|
||||
/>
|
||||
</template>
|
||||
@@ -423,6 +413,7 @@
|
||||
<ElInputNumber
|
||||
v-model="ruleGenT4RealEv"
|
||||
class="rule-gen-input-num"
|
||||
:disabled="true"
|
||||
:step="1"
|
||||
:controls="ruleGenInputControls"
|
||||
:size="ruleGenInputSize"
|
||||
@@ -451,8 +442,8 @@
|
||||
<ElInputNumber
|
||||
v-model="ruleGenT5RealEv"
|
||||
class="rule-gen-input-num"
|
||||
:disabled="true"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
:controls="ruleGenInputControls"
|
||||
:size="ruleGenInputSize"
|
||||
controls-position="right"
|
||||
@@ -472,6 +463,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<CreateRewardReferencePreviewDialog
|
||||
v-model="createRewardPreviewVisible"
|
||||
:dept-id="filterDeptId"
|
||||
@success="loadIndexList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -483,10 +480,11 @@
|
||||
useInjectedChannelDept
|
||||
} from '@/composables/useChannelDeptScope'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '../../api/reward_config/index'
|
||||
import { checkAuth } from '@/utils/tool'
|
||||
import CreateRewardReferencePreviewDialog from './modules/create-reward-reference-preview-dialog.vue'
|
||||
import {
|
||||
buildRowsFromTiers,
|
||||
computeBoardFrequencies,
|
||||
@@ -547,6 +545,7 @@
|
||||
const savingIndex = ref(false)
|
||||
const savingBigwin = ref(false)
|
||||
const createRewardLoading = ref(false)
|
||||
const createRewardPreviewVisible = ref(false)
|
||||
const ruleGenerateDialogVisible = ref(false)
|
||||
const ruleGenSubmitting = ref(false)
|
||||
const ruleGenT1Fixed = ref(3)
|
||||
@@ -593,15 +592,34 @@
|
||||
row.ui_text_en = text
|
||||
}
|
||||
|
||||
function rowRealEvNumber(row: IndexRow): number {
|
||||
return typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
|
||||
? row.real_ev
|
||||
: Number(row.real_ev)
|
||||
}
|
||||
|
||||
function syncRowTierFromRealEv(row: IndexRow) {
|
||||
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||
if (tier !== '') {
|
||||
row.tier = tier
|
||||
}
|
||||
}
|
||||
|
||||
function displayRowTier(row: IndexRow): string {
|
||||
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||
return tier !== '' ? tier : row.tier || '-'
|
||||
}
|
||||
|
||||
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')
|
||||
if (tier === 'T4') {
|
||||
row.ui_text = t('page.configPage.tierRecommendT4UiText')
|
||||
row.ui_text_en = t('page.configPage.tierRecommendT4UiTextEn')
|
||||
} else {
|
||||
applyRealEvDisplay(row, ev)
|
||||
}
|
||||
syncRowTierFromRealEv(row)
|
||||
}
|
||||
|
||||
/** 奖励索引 id 与后端 DiceRewardConfigLogic 一致:0~25 */
|
||||
@@ -655,16 +673,13 @@
|
||||
}
|
||||
|
||||
function handleRealEvChange(row: IndexRow) {
|
||||
const n =
|
||||
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
|
||||
? row.real_ev
|
||||
: Number(row.real_ev)
|
||||
applyRealEvDisplay(row, n)
|
||||
if (canTierRecommend.value && autoMatchTierOnRealEv.value) {
|
||||
const tier = inferTierFromRealEv(n)
|
||||
if (tier !== '') {
|
||||
row.tier = tier
|
||||
}
|
||||
const n = rowRealEvNumber(row)
|
||||
syncRowTierFromRealEv(row)
|
||||
if (row.tier === 'T4') {
|
||||
row.ui_text = t('page.configPage.tierRecommendT4UiText')
|
||||
row.ui_text_en = t('page.configPage.tierRecommendT4UiTextEn')
|
||||
} else {
|
||||
applyRealEvDisplay(row, n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,7 +689,7 @@
|
||||
}
|
||||
let count = 0
|
||||
for (const row of indexRowsExcludeBigwin.value) {
|
||||
const tier = row.tier
|
||||
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||
if (tier === 'T1' || tier === 'T2' || tier === 'T3' || tier === 'T4' || tier === 'T5') {
|
||||
applyRecommendRealEvToRow(row, tier)
|
||||
count++
|
||||
@@ -693,13 +708,8 @@
|
||||
}
|
||||
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
|
||||
syncRowTierFromRealEv(row)
|
||||
if (row.tier !== '') {
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -718,43 +728,7 @@
|
||||
}
|
||||
|
||||
async function handleCreateRewardReference() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('page.configPage.confirmCreateRefMsg'),
|
||||
t('page.configPage.confirmCreateRefTitle'),
|
||||
{
|
||||
confirmButtonText: t('page.configPage.confirmCreateRefOk'),
|
||||
cancelButtonText: t('page.configPage.confirmCreateRefCancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
createRewardLoading.value = true
|
||||
try {
|
||||
const res: any = await api.createRewardReference({ dept_id: filterDeptId.value as number })
|
||||
const data = res?.data ?? res
|
||||
let msg = t('page.configPage.createRefSuccessSimple')
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
const skipped = Number(data.skipped ?? 0)
|
||||
const skippedPart =
|
||||
skipped > 0 ? t('page.configPage.createRefSuccessSkipped', { n: skipped }) : ''
|
||||
msg = t('page.configPage.createRefSuccess', {
|
||||
cwNew: data.created_clockwise ?? 0,
|
||||
ccwNew: data.created_counterclockwise ?? 0,
|
||||
cwUp: data.updated_clockwise ?? 0,
|
||||
ccwUp: data.updated_counterclockwise ?? 0,
|
||||
skippedPart
|
||||
})
|
||||
}
|
||||
ElMessage.success(msg)
|
||||
loadIndexList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? t('page.configPage.createRefFail'))
|
||||
} finally {
|
||||
createRewardLoading.value = false
|
||||
}
|
||||
createRewardPreviewVisible.value = true
|
||||
}
|
||||
|
||||
function extractIndexList(res: unknown): Record<string, unknown>[] {
|
||||
@@ -779,6 +753,11 @@
|
||||
.list({ saiType: 'all', limit: 200, dept_id: filterDeptId.value })
|
||||
.then((res: unknown) => {
|
||||
const rows = extractIndexList(res).map((r) => normalizeIndexRow(r))
|
||||
for (const row of rows) {
|
||||
if (row.tier !== 'BIGWIN') {
|
||||
syncRowTierFromRealEv(row)
|
||||
}
|
||||
}
|
||||
indexRows.value = rows
|
||||
indexRowsSnapshot = rows.map((r) => ({ ...r }))
|
||||
})
|
||||
@@ -1024,6 +1003,9 @@
|
||||
return
|
||||
}
|
||||
const toSave = indexRows.value.filter((r) => r.tier !== 'BIGWIN')
|
||||
for (const row of toSave) {
|
||||
syncRowTierFromRealEv(row)
|
||||
}
|
||||
savingIndex.value = true
|
||||
try {
|
||||
const indexPayload = toSave.map((r) => ({
|
||||
@@ -1239,6 +1221,10 @@
|
||||
min-width: 100px;
|
||||
max-width: 160px;
|
||||
}
|
||||
.tier-readonly {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.tier-recommend-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
:title="$t('page.configPage.createRefPreviewTitle')"
|
||||
width="980px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-loading="loading" class="dialog-body">
|
||||
<div class="preview-tip">
|
||||
<div v-if="meta.unchanged" class="tip-line">
|
||||
{{ $t('page.configPage.createRefPreviewTipUnchanged') }}
|
||||
</div>
|
||||
<div v-else class="tip-line">
|
||||
{{ $t('page.configPage.createRefPreviewTipChanged') }}
|
||||
</div>
|
||||
<div class="tip-line" v-if="meta.skipped > 0">
|
||||
{{ $t('page.configPage.createRefPreviewSkipped', { n: meta.skipped }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElTabs v-model="activeDirection" type="card" class="direction-tabs">
|
||||
<ElTabPane :label="$t('page.configPage.createRefPreviewClockwise')" name="0">
|
||||
<ElTabs v-model="activeTier" type="card" class="tier-tabs">
|
||||
<ElTabPane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
|
||||
<ElTable
|
||||
:data="getTierItems(t, 0)"
|
||||
:row-class-name="rowClassName"
|
||||
border
|
||||
size="small"
|
||||
class="preview-table"
|
||||
>
|
||||
<ElTableColumn :label="$t('page.table.dicePoints')" prop="grid_number" width="120" align="center" />
|
||||
<ElTableColumn label="start_index" prop="start_index" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.endIndex')" prop="id" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.tier')" prop="tier" width="90" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.displayText')" prop="ui_text" min-width="110" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.realEv')" prop="real_ev" width="110" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.remark')" prop="remark" min-width="140" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.weight')" prop="weight" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.configPage.createRefPreviewDiff')" min-width="220" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row?.diff_changed" class="diff-text">
|
||||
{{ formatDiff(row) }}
|
||||
</span>
|
||||
<span v-else class="diff-text diff-text-ok">{{ $t('page.configPage.createRefPreviewNoDiff') }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane :label="$t('page.configPage.createRefPreviewCounterclockwise')" name="1">
|
||||
<ElTabs v-model="activeTier" type="card" class="tier-tabs">
|
||||
<ElTabPane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
|
||||
<ElTable
|
||||
:data="getTierItems(t, 1)"
|
||||
:row-class-name="rowClassName"
|
||||
border
|
||||
size="small"
|
||||
class="preview-table"
|
||||
>
|
||||
<ElTableColumn :label="$t('page.table.dicePoints')" prop="grid_number" width="120" align="center" />
|
||||
<ElTableColumn label="start_index" prop="start_index" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.endIndex')" prop="id" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.tier')" prop="tier" width="90" align="center" />
|
||||
<ElTableColumn :label="$t('page.table.displayText')" prop="ui_text" min-width="110" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.realEv')" prop="real_ev" width="110" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.remark')" prop="remark" min-width="140" align="center" show-overflow-tooltip />
|
||||
<ElTableColumn :label="$t('page.table.weight')" prop="weight" width="110" align="center" />
|
||||
<ElTableColumn :label="$t('page.configPage.createRefPreviewDiff')" min-width="220" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row?.diff_changed" class="diff-text">
|
||||
{{ formatDiff(row) }}
|
||||
</span>
|
||||
<span v-else class="diff-text diff-text-ok">{{ $t('page.configPage.createRefPreviewNoDiff') }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="refreshPreview" :loading="loading" v-ripple>{{
|
||||
$t('page.configPage.createRefPreviewRefresh')
|
||||
}}</ElButton>
|
||||
<ElButton @click="handleClose" v-ripple>{{ $t('common.cancel') }}</ElButton>
|
||||
<ElButton type="primary" :loading="submitting" @click="handleImport" v-ripple>{{
|
||||
$t('page.configPage.createRefPreviewImport')
|
||||
}}</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '../../../api/reward_config/index'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||
const tierKeys = TIER_KEYS
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
deptId: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
deptId: 0
|
||||
})
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const activeDirection = ref<'0' | '1'>('0')
|
||||
const activeTier = ref('T1')
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const meta = reactive<{ unchanged: boolean; skipped: number }>({ unchanged: false, skipped: 0 })
|
||||
const preview = ref<Record<string, { 0: any[]; 1: any[] }>>({})
|
||||
|
||||
function rowClassName({ row }: { row: any }): string {
|
||||
return row?.diff_changed ? 'row-diff' : ''
|
||||
}
|
||||
|
||||
function formatDiff(row: any): string {
|
||||
if (!row) return ''
|
||||
const oldStart = row.old_start_index != null ? String(row.old_start_index) : '-'
|
||||
const oldEnd = row.old_end_index != null ? String(row.old_end_index) : '-'
|
||||
const oldTier = row.old_tier != null ? String(row.old_tier) : '-'
|
||||
const newStart = row.start_index != null ? String(row.start_index) : '-'
|
||||
const newEnd = row.id != null ? String(row.id) : '-'
|
||||
const newTier = row.tier != null ? String(row.tier) : '-'
|
||||
return `${oldStart}/${oldEnd}/${oldTier} → ${newStart}/${newEnd}/${newTier}`
|
||||
}
|
||||
|
||||
function getTierItems(tier: string, direction: 0 | 1): any[] {
|
||||
const tierData = preview.value?.[tier]
|
||||
if (!tierData) return []
|
||||
const rows = direction === 0 ? tierData[0] : tierData[1]
|
||||
return Array.isArray(rows) ? rows : []
|
||||
}
|
||||
|
||||
async function refreshPreview() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await api.createRewardReferencePreview({ dept_id: props.deptId })
|
||||
const data = res?.data ?? res
|
||||
meta.unchanged = Boolean(data?.unchanged)
|
||||
meta.skipped = Number(data?.skipped ?? 0)
|
||||
preview.value = (data?.preview ?? {}) as any
|
||||
activeDirection.value = '0'
|
||||
activeTier.value = 'T1'
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? 'preview failed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
submitting.value = true
|
||||
try {
|
||||
const res: any = await api.createRewardReference({ dept_id: props.deptId })
|
||||
const data = res?.data ?? res
|
||||
if (data?.unchanged) {
|
||||
ElMessage.success(t('page.configPage.createRefPreviewImportNoop'))
|
||||
} else {
|
||||
ElMessage.success(t('page.configPage.createRefPreviewImportOk'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? 'import failed')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
refreshPreview()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-body {
|
||||
min-height: 240px;
|
||||
}
|
||||
.preview-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.tip-line + .tip-line {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 8px;
|
||||
}
|
||||
:deep(.row-diff td) {
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
.diff-text {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
.diff-text-ok {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,38 +58,36 @@ export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
||||
T1: 3,
|
||||
T2: 1.5,
|
||||
T3: 0.5,
|
||||
T4: -0.4,
|
||||
T5: 0
|
||||
T4: 0,
|
||||
T5: -0.4
|
||||
}
|
||||
|
||||
/**
|
||||
* 按结算金额推断档位(与奖励配置页规则说明一致)
|
||||
* 结算金额 < 0 → T4;0 < 结算金额 < 1 → T3;1 < 结算金额 < 2 → T2;2 < 结算金额 → T1;T5 结算金额 = 0
|
||||
* T1 大奖:>2
|
||||
* T2 小赚:2>=金额>1
|
||||
* T3 抽水:1>=金额>0
|
||||
* T4 再来一次:=0
|
||||
* T5 惩罚: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) {
|
||||
if (realEv > 1 && realEv <= 2) {
|
||||
return 'T2'
|
||||
}
|
||||
if (realEv > 0 && realEv < 1) {
|
||||
if (realEv > 0 && realEv <= 1) {
|
||||
return 'T3'
|
||||
}
|
||||
if (realEv === 1) {
|
||||
return 'T2'
|
||||
if (realEv === 0) {
|
||||
return 'T4'
|
||||
}
|
||||
if (realEv === 2) {
|
||||
return 'T1'
|
||||
if (realEv < 0) {
|
||||
return 'T5'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -101,16 +99,16 @@ export function validateTierRealEvStandards(s: TierRealEvStandards): string | nu
|
||||
if (!Number.isFinite(s.T1) || !(s.T1 > 2)) {
|
||||
return 'ruleGenInvalidT1RealEv'
|
||||
}
|
||||
if (!Number.isFinite(s.T2) || !(s.T2 > 1 && s.T2 < 2)) {
|
||||
if (!Number.isFinite(s.T2) || !(s.T2 > 1 && s.T2 <= 2)) {
|
||||
return 'ruleGenInvalidT2RealEv'
|
||||
}
|
||||
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 < 1)) {
|
||||
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 <= 1)) {
|
||||
return 'ruleGenInvalidT3RealEv'
|
||||
}
|
||||
if (!Number.isFinite(s.T4) || !(s.T4 < 0)) {
|
||||
if (!Number.isFinite(s.T4) || s.T4 !== 0) {
|
||||
return 'ruleGenInvalidT4RealEv'
|
||||
}
|
||||
if (!Number.isFinite(s.T5) || s.T5 !== 0) {
|
||||
if (!Number.isFinite(s.T5) || s.T5 >= 0) {
|
||||
return 'ruleGenInvalidT5RealEv'
|
||||
}
|
||||
return null
|
||||
@@ -307,7 +305,7 @@ function uiTextByTierWhenStandards(
|
||||
tier: IndexTier,
|
||||
realEv: number
|
||||
): { ui_text: string; ui_text_en: string } {
|
||||
if (tier === 'T5') {
|
||||
if (tier === 'T4') {
|
||||
return { ui_text: '再来一次', ui_text_en: 'Once again' }
|
||||
}
|
||||
const value = Number.isFinite(realEv) ? realEv.toFixed(2) : String(realEv)
|
||||
@@ -379,13 +377,13 @@ export function buildRowsFromTiers(
|
||||
const f = uiTextByTierWhenStandards(tier, real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '惩罚'
|
||||
remark = '再来一次'
|
||||
} else {
|
||||
real_ev = standards.T5
|
||||
const f = uiTextByTierWhenStandards(tier, real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '前端需要在播放一次动画(特殊)'
|
||||
remark = '惩罚'
|
||||
}
|
||||
} else if (tier === 'T1') {
|
||||
real_ev = 101 + ((id * 17 + grid_number * 3) % 398)
|
||||
@@ -417,18 +415,17 @@ export function buildRowsFromTiers(
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '抽水'
|
||||
} else if (tier === 'T4') {
|
||||
real_ev = 0
|
||||
ui_text = '再来一次'
|
||||
ui_text_en = 'Once again'
|
||||
remark = '再来一次'
|
||||
} else {
|
||||
t4Seq++
|
||||
real_ev = -101 - t4Seq * 15
|
||||
const f = uiTextFromRealEv(real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '惩罚'
|
||||
} else {
|
||||
real_ev = 0
|
||||
const f = uiTextFromRealEv(real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '前端需要在播放一次动画(特殊)'
|
||||
}
|
||||
rows.push({
|
||||
id,
|
||||
|
||||
@@ -263,6 +263,23 @@ class DiceRewardConfigController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建奖励对照(预览):不写入 dice_reward,仅计算并返回预览分组数据。
|
||||
* 若当前 dice_reward 与计算结果一致,则 unchanged=true,并在预览中复用现有权重(导入时仍沿用旧权重)。
|
||||
*/
|
||||
#[Permission('创建奖励对照', 'dice:reward_config:index:createRewardReference')]
|
||||
public function createRewardReferencePreview(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$rewardLogic = new DiceRewardLogic();
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$result = $rewardLogic->createRewardReferencePreviewFromConfig($deptId);
|
||||
return $this->success($result, 'preview reward mapping success');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
|
||||
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID,用于设定 T1-T5 概率
|
||||
|
||||
@@ -327,6 +327,79 @@ class DiceRewardLogic
|
||||
private const GRID_NUMBER_MIN = 5;
|
||||
private const GRID_NUMBER_MAX = 30;
|
||||
|
||||
/**
|
||||
* 预览:按当前 dice_reward_config 计算将要生成的 dice_reward(不写库)
|
||||
* 若当前 dice_reward 与计算结果完全一致,则标记 unchanged=true,并返回现有权重(导入时将复用旧权重)
|
||||
*
|
||||
* @return array{unchanged: bool, skipped: int, preview: array<string, array{0: array, 1: array}>}
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function createRewardReferencePreviewFromConfig(?int $deptId = null): array
|
||||
{
|
||||
$normalizedDeptId = $deptId;
|
||||
$list = $this->loadConfigListForReference($normalizedDeptId);
|
||||
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
|
||||
|
||||
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
|
||||
$compare = $this->compareReferenceRows($computed['rows'], $existing);
|
||||
$unchanged = $compare['unchanged'];
|
||||
|
||||
$previewRows = [];
|
||||
foreach ($computed['rows'] as $row) {
|
||||
$key = $row['direction'] . ':' . $row['grid_number'];
|
||||
$weight = self::WEIGHT_MIN;
|
||||
$oldStart = null;
|
||||
$oldEnd = null;
|
||||
$oldTier = null;
|
||||
$oldWeight = null;
|
||||
if (isset($existing[$key])) {
|
||||
$oldStart = isset($existing[$key]['start_index']) ? (int) $existing[$key]['start_index'] : null;
|
||||
$oldEnd = isset($existing[$key]['end_index']) ? (int) $existing[$key]['end_index'] : null;
|
||||
$oldTier = isset($existing[$key]['tier']) ? (string) $existing[$key]['tier'] : null;
|
||||
$oldWeight = isset($existing[$key]['weight']) ? (int) $existing[$key]['weight'] : null;
|
||||
}
|
||||
if ($unchanged && $oldWeight !== null) {
|
||||
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $oldWeight));
|
||||
}
|
||||
|
||||
$diffChanged = false;
|
||||
$diffFields = [];
|
||||
if ($oldStart === null || $oldEnd === null || $oldTier === null) {
|
||||
$diffChanged = true;
|
||||
$diffFields[] = 'new';
|
||||
} else {
|
||||
if ((int) $oldStart !== (int) ($row['start_index'] ?? 0)) {
|
||||
$diffChanged = true;
|
||||
$diffFields[] = 'start_index';
|
||||
}
|
||||
if ((int) $oldEnd !== (int) ($row['end_index'] ?? 0)) {
|
||||
$diffChanged = true;
|
||||
$diffFields[] = 'end_index';
|
||||
}
|
||||
if (trim((string) $oldTier) !== trim((string) ($row['tier'] ?? ''))) {
|
||||
$diffChanged = true;
|
||||
$diffFields[] = 'tier';
|
||||
}
|
||||
}
|
||||
|
||||
$previewRows[] = array_merge($row, [
|
||||
'weight' => $weight,
|
||||
'old_start_index' => $oldStart,
|
||||
'old_end_index' => $oldEnd,
|
||||
'old_tier' => $oldTier,
|
||||
'old_weight' => $oldWeight,
|
||||
'diff_changed' => $diffChanged,
|
||||
'diff_fields' => $diffFields,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'unchanged' => $unchanged,
|
||||
'skipped' => $computed['skipped'],
|
||||
'preview' => $this->groupReferenceRowsByTierWithDirection($previewRows),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
|
||||
*
|
||||
@@ -352,10 +425,76 @@ class DiceRewardLogic
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function createRewardReferenceFromConfig(?int $deptId = null): array
|
||||
{
|
||||
$normalizedDeptId = $deptId;
|
||||
$list = $this->loadConfigListForReference($normalizedDeptId);
|
||||
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
|
||||
|
||||
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
|
||||
$compare = $this->compareReferenceRows($computed['rows'], $existing);
|
||||
if ($compare['unchanged']) {
|
||||
return [
|
||||
'created_clockwise' => 0,
|
||||
'created_counterclockwise' => 0,
|
||||
'updated_clockwise' => 0,
|
||||
'updated_counterclockwise' => 0,
|
||||
'skipped' => $computed['skipped'],
|
||||
'unchanged' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$table = (new DiceReward())->getTable();
|
||||
if ($normalizedDeptId === null) {
|
||||
Db::table($table)->whereNull('dept_id')->delete();
|
||||
} else {
|
||||
Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
|
||||
}
|
||||
|
||||
$createdCw = 0;
|
||||
$createdCcw = 0;
|
||||
foreach ($computed['rows'] as $row) {
|
||||
$m = new DiceReward();
|
||||
$m->tier = $row['tier'];
|
||||
$m->direction = (int) $row['direction'];
|
||||
$m->end_index = (int) $row['end_index'];
|
||||
$m->weight = self::WEIGHT_MIN;
|
||||
$m->grid_number = (int) $row['grid_number'];
|
||||
$m->start_index = (int) $row['start_index'];
|
||||
$m->ui_text = (string) ($row['ui_text'] ?? '');
|
||||
$m->real_ev = $row['real_ev'] ?? null;
|
||||
$m->remark = (string) ($row['remark'] ?? '');
|
||||
$m->type = isset($row['type']) ? (int) $row['type'] : 0;
|
||||
if ($normalizedDeptId !== null) {
|
||||
$m->dept_id = $normalizedDeptId;
|
||||
}
|
||||
$m->save();
|
||||
if ((int) $row['direction'] === DiceReward::DIRECTION_CLOCKWISE) {
|
||||
$createdCw++;
|
||||
} else {
|
||||
$createdCcw++;
|
||||
}
|
||||
}
|
||||
|
||||
DiceReward::refreshCache($normalizedDeptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
return [
|
||||
'created_clockwise' => $createdCw,
|
||||
'created_counterclockwise' => $createdCcw,
|
||||
'updated_clockwise' => 0,
|
||||
'updated_counterclockwise' => 0,
|
||||
'skipped' => $computed['skipped'],
|
||||
'unchanged' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取奖励配置(按 id asc),并把模板 dept 转为 null
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function loadConfigListForReference(?int &$deptId): array
|
||||
{
|
||||
$configQuery = DiceRewardConfig::order('id', 'asc');
|
||||
if ($deptId === null || $deptId === \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||
$templateId = \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
$configQuery->where(function ($q) use ($templateId) {
|
||||
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
|
||||
});
|
||||
@@ -367,25 +506,25 @@ class DiceRewardLogic
|
||||
if (empty($list)) {
|
||||
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
|
||||
}
|
||||
$configCount = count($list);
|
||||
if ($configCount < self::BOARD_SIZE) {
|
||||
if (count($list) < self::BOARD_SIZE) {
|
||||
throw new ApiException(
|
||||
\app\api\util\ApiLang::translateParams(
|
||||
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
||||
[$configCount]
|
||||
[count($list)]
|
||||
)
|
||||
);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
$table = (new DiceReward())->getTable();
|
||||
if ($deptId === null) {
|
||||
Db::table($table)->whereNull('dept_id')->delete();
|
||||
} else {
|
||||
Db::table($table)->where('dept_id', $deptId)->delete();
|
||||
}
|
||||
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
|
||||
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
|
||||
/**
|
||||
* 计算 5-30 两个方向的对照行(不含权重)
|
||||
* @param array<int, array<string, mixed>> $list
|
||||
* @return array{rows: array<int, array<string, mixed>>, skipped: int}
|
||||
*/
|
||||
private function computeReferenceRowsFromConfigList(array $list, ?int $deptId): array
|
||||
{
|
||||
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos]
|
||||
$gridToPosition = [];
|
||||
foreach ($list as $pos => $row) {
|
||||
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
||||
@@ -394,12 +533,8 @@ class DiceRewardLogic
|
||||
}
|
||||
}
|
||||
|
||||
$createdCw = 0;
|
||||
$createdCcw = 0;
|
||||
$updatedCw = 0;
|
||||
$updatedCcw = 0;
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
|
||||
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
|
||||
if (!isset($gridToPosition[$gridNumber])) {
|
||||
$skipped++;
|
||||
@@ -414,115 +549,131 @@ class DiceRewardLogic
|
||||
|
||||
$configCw = $list[$endPosCw] ?? null;
|
||||
$configCcw = $list[$endPosCcw] ?? null;
|
||||
$endIdCw = $configCw !== null && isset($configCw['id']) ? (int) $configCw['id'] : 0;
|
||||
$endIdCcw = $configCcw !== null && isset($configCcw['id']) ? (int) $configCcw['id'] : 0;
|
||||
|
||||
if ($configCw !== null) {
|
||||
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
|
||||
if ($tier !== '') {
|
||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
||||
$weightCw = isset($configCw['weight']) && $configCw['weight'] !== null
|
||||
? $configCw['weight']
|
||||
: self::WEIGHT_MIN;
|
||||
$payloadCw = [
|
||||
$rows[] = [
|
||||
'tier' => $tier,
|
||||
'weight' => $weightCw,
|
||||
'direction' => DiceReward::DIRECTION_CLOCKWISE,
|
||||
'weight' => self::WEIGHT_MIN,
|
||||
'grid_number' => $gridNumber,
|
||||
'start_index' => $startId,
|
||||
'end_index' => $endIdCw,
|
||||
'end_index' => isset($configCw['id']) ? (int) $configCw['id'] : 0,
|
||||
'ui_text' => $configCw['ui_text'] ?? '',
|
||||
'real_ev' => $configCw['real_ev'] ?? null,
|
||||
'remark' => $configCw['remark'] ?? '',
|
||||
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
|
||||
];
|
||||
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber);
|
||||
if ($deptId === null) {
|
||||
$existingQuery->whereNull('dept_id');
|
||||
} else {
|
||||
$existingQuery->where('dept_id', $deptId);
|
||||
}
|
||||
$existing = $existingQuery->find();
|
||||
if ($existing) {
|
||||
DiceReward::where('id', $existing->id)->update($payloadCw);
|
||||
$updatedCw++;
|
||||
} else {
|
||||
$m = new DiceReward();
|
||||
$m->tier = $tier;
|
||||
$m->direction = DiceReward::DIRECTION_CLOCKWISE;
|
||||
$m->end_index = $endIdCw;
|
||||
$m->weight = $weightCw;
|
||||
$m->grid_number = $gridNumber;
|
||||
$m->start_index = $startId;
|
||||
$m->ui_text = $configCw['ui_text'] ?? '';
|
||||
$m->real_ev = $configCw['real_ev'] ?? null;
|
||||
$m->remark = $configCw['remark'] ?? '';
|
||||
$m->type = isset($configCw['type']) ? (int) $configCw['type'] : 0;
|
||||
if ($deptId !== null) {
|
||||
$m->dept_id = $deptId;
|
||||
}
|
||||
$m->save();
|
||||
$createdCw++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($configCcw !== null) {
|
||||
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
|
||||
if ($tier !== '') {
|
||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
||||
$weightCcw = isset($configCcw['weight']) && $configCcw['weight'] !== null
|
||||
? $configCcw['weight']
|
||||
: self::WEIGHT_MIN;
|
||||
$payloadCcw = [
|
||||
$rows[] = [
|
||||
'tier' => $tier,
|
||||
'weight' => $weightCcw,
|
||||
'direction' => DiceReward::DIRECTION_COUNTERCLOCKWISE,
|
||||
'weight' => self::WEIGHT_MIN,
|
||||
'grid_number' => $gridNumber,
|
||||
'start_index' => $startId,
|
||||
'end_index' => $endIdCcw,
|
||||
'end_index' => isset($configCcw['id']) ? (int) $configCcw['id'] : 0,
|
||||
'ui_text' => $configCcw['ui_text'] ?? '',
|
||||
'real_ev' => $configCcw['real_ev'] ?? null,
|
||||
'remark' => $configCcw['remark'] ?? '',
|
||||
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
|
||||
];
|
||||
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber);
|
||||
if ($deptId === null) {
|
||||
$existingQuery->whereNull('dept_id');
|
||||
} else {
|
||||
$existingQuery->where('dept_id', $deptId);
|
||||
}
|
||||
$existing = $existingQuery->find();
|
||||
if ($existing) {
|
||||
DiceReward::where('id', $existing->id)->update($payloadCcw);
|
||||
$updatedCcw++;
|
||||
} else {
|
||||
$m = new DiceReward();
|
||||
$m->tier = $tier;
|
||||
$m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE;
|
||||
$m->end_index = $endIdCcw;
|
||||
$m->weight = $weightCcw;
|
||||
$m->grid_number = $gridNumber;
|
||||
$m->start_index = $startId;
|
||||
$m->ui_text = $configCcw['ui_text'] ?? '';
|
||||
$m->real_ev = $configCcw['real_ev'] ?? null;
|
||||
$m->remark = $configCcw['remark'] ?? '';
|
||||
$m->type = isset($configCcw['type']) ? (int) $configCcw['type'] : 0;
|
||||
if ($deptId !== null) {
|
||||
$m->dept_id = $deptId;
|
||||
}
|
||||
$m->save();
|
||||
$createdCcw++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['rows' => $rows, 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
return [
|
||||
'created_clockwise' => $createdCw,
|
||||
'created_counterclockwise' => $createdCcw,
|
||||
'updated_clockwise' => $updatedCw,
|
||||
'updated_counterclockwise' => $updatedCcw,
|
||||
'skipped' => $skipped,
|
||||
];
|
||||
/**
|
||||
* 读出当前 dice_reward(用于对比/复用权重)。key = "direction:grid_number"
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function loadExistingRewardRowsForReference(?int $deptId): array
|
||||
{
|
||||
$query = DiceReward::whereIn('grid_number', range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX))
|
||||
->whereIn('direction', [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE]);
|
||||
if ($deptId === null) {
|
||||
$query->whereNull('dept_id');
|
||||
} else {
|
||||
$query->where('dept_id', $deptId);
|
||||
}
|
||||
$rows = $query->select()->toArray();
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
|
||||
$gn = isset($r['grid_number']) ? (int) $r['grid_number'] : 0;
|
||||
$map[$dir . ':' . $gn] = $r;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比 computed 与 existing 是否完全一致(忽略权重)
|
||||
* @param array<int, array<string, mixed>> $computedRows
|
||||
* @param array<string, array<string, mixed>> $existingMap
|
||||
* @return array{unchanged: bool}
|
||||
*/
|
||||
private function compareReferenceRows(array $computedRows, array $existingMap): array
|
||||
{
|
||||
if (empty($computedRows)) {
|
||||
return ['unchanged' => false];
|
||||
}
|
||||
foreach ($computedRows as $row) {
|
||||
$key = $row['direction'] . ':' . $row['grid_number'];
|
||||
if (!isset($existingMap[$key])) {
|
||||
return ['unchanged' => false];
|
||||
}
|
||||
$ex = $existingMap[$key];
|
||||
$same =
|
||||
((int) ($ex['start_index'] ?? 0) === (int) ($row['start_index'] ?? 0)) &&
|
||||
((int) ($ex['end_index'] ?? 0) === (int) ($row['end_index'] ?? 0)) &&
|
||||
(trim((string) ($ex['tier'] ?? '')) === trim((string) ($row['tier'] ?? '')));
|
||||
if (!$same) {
|
||||
return ['unchanged' => false];
|
||||
}
|
||||
}
|
||||
return ['unchanged' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将行按 tier -> {0:[],1:[]} 组织,便于前端展示(与 weightRatioList 输出结构一致)
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
* @return array<string, array{0: array, 1: array}>
|
||||
*/
|
||||
private function groupReferenceRowsByTierWithDirection(array $rows): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::TIER_KEYS as $tier) {
|
||||
$result[$tier] = [0 => [], 1 => []];
|
||||
}
|
||||
foreach ($rows as $r) {
|
||||
$tier = isset($r['tier']) ? trim((string) $r['tier']) : '';
|
||||
if ($tier === '' || !isset($result[$tier])) {
|
||||
continue;
|
||||
}
|
||||
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
|
||||
$dir = $dir === 1 ? 1 : 0;
|
||||
$result[$tier][$dir][] = [
|
||||
'reward_id' => 0,
|
||||
'id' => isset($r['end_index']) ? (int) $r['end_index'] : 0,
|
||||
'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
|
||||
'ui_text' => (string) ($r['ui_text'] ?? ''),
|
||||
'real_ev' => $r['real_ev'] ?? 0,
|
||||
'remark' => (string) ($r['remark'] ?? ''),
|
||||
'weight' => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
|
||||
'start_index' => isset($r['start_index']) ? (int) $r['start_index'] : 0,
|
||||
'tier' => (string) ($r['tier'] ?? ''),
|
||||
'old_start_index' => $r['old_start_index'] ?? null,
|
||||
'old_end_index' => $r['old_end_index'] ?? null,
|
||||
'old_tier' => $r['old_tier'] ?? null,
|
||||
'old_weight' => $r['old_weight'] ?? null,
|
||||
'diff_changed' => (bool) ($r['diff_changed'] ?? false),
|
||||
'diff_fields' => $r['diff_fields'] ?? [],
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
其中地图的索引可以按照需求点击图中的按规则生成
|
||||
|
||||
并且规则尽可能符合:结算金额 < 0 → T4;0 < 结算金额 < 100 → T3;100 < 结算金额 < 200 → T2;200 < 结算金额 → T1;T5「再来一次」结算金额=0
|
||||
并且规则尽可能符合:结算金额>2 → T1;2>=结算金额>1 → T2;1>=结算金额>0 → T3;结算金额=0 → T4(再来一次);0>结算金额 → T5(惩罚)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ Route::group('/core', function () {
|
||||
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/createRewardReferencePreview', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReferencePreview']);
|
||||
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);
|
||||
fastRoute('dice/ante_config/DiceAnteConfig', \app\dice\controller\ante_config\DiceAnteConfigController::class);
|
||||
|
||||
Reference in New Issue
Block a user