+ }
required
size="small"
value={form.jackpot_multipliers}
@@ -355,21 +377,21 @@ export function LuckyGiftConfigSections({ appOptions, form, isCreate, onChange }
/>
@@ -389,7 +411,7 @@ export function LuckyGiftConfigSections({ appOptions, form, isCreate, onChange }
data-config-section="limits"
id="ops-config-section-limits"
>
-
@@ -447,12 +469,31 @@ function SectionHeading({ detail, id, ok = false, title }) {
);
}
+function JackpotFlowSummary() {
+ return (
+
+
+ 1. 消费达标优先
+ 当天累计消费每跨过一次门槛,就保存一次大奖资格;资格从下一抽开始使用。
+
+
+ 2. 返还偏低补偿
+ 没有待用资格时,整体、用户当天和近 72 小时平均返还都不高于触发线,才尝试大奖。
+
+
+ 3. 只从付得起的档位选
+ 先剔除奖池或限额不足的倍率,再在剩余倍率中等机会选择;消费资格付不起时会保留。
+
+
+ );
+}
+
function NumberField({ disabled = false, form, helperText, label, name, onChange, step = "1" }) {
return (
}
required
size="small"
slotProps={{ htmlInput: { inputMode: "decimal", min: 0, step } }}
diff --git a/ops-center/src/components/LuckyGiftFieldHelp.jsx b/ops-center/src/components/LuckyGiftFieldHelp.jsx
new file mode 100644
index 0000000..09fc0e1
--- /dev/null
+++ b/ops-center/src/components/LuckyGiftFieldHelp.jsx
@@ -0,0 +1,277 @@
+import HelpOutlineOutlined from "@mui/icons-material/HelpOutlineOutlined";
+import Tooltip from "@mui/material/Tooltip";
+
+// 字段说明集中维护,避免同一参数在不同分区出现互相矛盾的运营口径。
+// 每条都回答“控制什么、调大会怎样、发布时受什么限制”,Tooltip 只负责展示,不参与表单值或校验。
+const FIELD_HELP = {
+ app_code: {
+ summary: "决定这套幸运礼物规则属于哪个 App。不同 App 的配置、抽奖记录和奖池余额彼此隔离。",
+ adjust: "选择错 App 会把新版本发布到另一套业务数据中;编辑已有配置时不能在这里迁移 App。",
+ constraint: "发布前确认应用与实际送礼入口一致。",
+ },
+ pool_id: {
+ summary: "决定礼物使用哪一组规则和哪一本奖池账。同一 App 内,送礼接口会按这个 ID 找到配置。",
+ adjust: "改成新 ID 会创建一组新的规则,不会搬走旧奖池里的金币,也不会改写旧抽奖记录。",
+ constraint: "fixed_v2 与 dynamic_v3 即使使用相同 ID,也会分别记账;ID 必须与礼物接入配置一致。",
+ },
+ strategy_version: {
+ summary:
+ "选择开奖方式。fixed_v2 保留旧版固定规则;dynamic_v3 会根据奖池余额、充值阶段、大奖条件和返奖上限动态调整。",
+ adjust: "切到 dynamic_v3 会载入安全草稿并保持停用,确认全部金额口径后再发布。",
+ constraint: "dynamic_v3 使用独立奖池且从 0 开始,发布规则不会自动注入金币。",
+ },
+ enabled: {
+ summary: "决定新版本发布后是否立即用于真实送礼。关闭时规则仍会保存,但不会触发幸运开奖。",
+ adjust: "首次配置或大幅调整时可先保持停用,核对资金比例和上限后再启用。",
+ constraint: "启用 dynamic_v3 前必须填完大奖消费门槛和所有返奖上限。",
+ },
+ target_rtp_percent: {
+ summary: "表示基础奖档的配置目标。例如填 98,代表各阶段的“倍率 × 基础概率”预计返还约为 98%。",
+ adjust: "修改后,页面会重新分配各档基础概率;调高通常增加基础返奖,调低则相反。",
+ constraint:
+ "只能填 10%–99%,进入奖池的比例不能低于它。水位、充值加成、大奖和上限都会改变实际结果,它不是线上返还承诺。",
+ },
+ control_band_percent: {
+ summary: "允许每个用户阶段的奖档组合与平均返还目标存在多大偏差。",
+ adjust: "调大更容易通过发布,但实际返还可能离目标更远;调小更严格,也更难配置出可发布的倍率组合。",
+ constraint: "必须大于 0 且不超过 5 个百分点,不会强行补偿单次或单个用户的结果。",
+ },
+ pool_rate_percent: {
+ summary: "每笔幸运礼物消费中,有多少金币进入公共奖池,后续中奖从这里支付。",
+ adjust: "调高会让奖池补充更快;调低会留出更多平台或收礼份额,但奖池更容易不足。",
+ constraint: "不能低于平均返还目标;V3 中它与平台、收礼份额合计必须等于 100%,按整数金币向下取整。",
+ },
+ profit_rate_percent: {
+ summary: "V3 每笔消费中直接计为平台收入的比例,不进入奖池,也不发给收礼对象。",
+ adjust: "调高会增加平台份额,但必须相应减少奖池或收礼份额。",
+ constraint:
+ "不能为负数;V3 三项资金比例必须精确合计 100%,整数拆分后不能整除的余数也计入平台;V2 不使用该比例开奖。",
+ },
+ anchor_rate_percent: {
+ summary: "V3 每笔消费中返给本次收礼对象的比例,通常是主播但不一定等于房主;这部分不是用户中奖奖励。",
+ adjust: "调高会增加收礼对象所得,并相应减少奖池或平台份额。",
+ constraint:
+ "按整数金币向下取整,且必须与钱包实际发给收礼对象的金币完全一致,否则 V3 会拒绝开奖;V2 不使用该比例开奖。",
+ },
+ settlement_window_wager: {
+ summary: "V3 按实际消费金币累计一轮返还观察窗口;某一抽跨过门槛时仍归旧窗口,下一抽才进入新窗口。",
+ adjust: "调大后更少重开窗口;调小后会更频繁清空全局窗口样本。V2 会先按礼物参考金额换算成窗口抽数。",
+ constraint: "必须大于 0。新窗口还没有消费样本时,不会触发返还偏低补偿大奖。",
+ },
+ gift_price_reference: {
+ summary: "V2 用它把累计消费换算成“折算次数”,同时计算用户阶段和每轮窗口的抽数;它不是礼物真实售价。",
+ adjust: "调高后,同样消费折算成更少次数,用户升级更慢且 V2 窗口抽数更少;调低则相反。",
+ constraint: "必须大于 0。V3 的窗口直接看真实金币流水,用户阶段看 7 日和 30 日充值,不用它做这两项判断。",
+ },
+ novice_max_equivalent_draws: {
+ summary: "fixed_v2 中,用户累计消费折算次数不超过这个值时使用新手奖档。",
+ adjust: "调高会让用户在新手阶段停留更久;调低会更早进入普通阶段。",
+ constraint: "不能小于 0,也不能高于普通阶段上限;dynamic_v3 不使用这个值分层。",
+ },
+ normal_max_equivalent_draws: {
+ summary: "fixed_v2 中,用户超过新手上限后,不超过这个值时使用普通奖档;再超过才进入高阶奖档。",
+ adjust: "调高会推迟进入高阶阶段;调低会让用户更早进入高阶阶段。",
+ constraint: "不能小于新手阶段上限;dynamic_v3 不使用这个值分层。",
+ },
+ initial_pool_coins: {
+ summary: "表示发布规则时是否顺便给奖池放入启动金币。dynamic_v3 已改为不允许规则自动注资。",
+ adjust: "该值固定为 0 且不可编辑,避免每次发新版本重复增加奖池余额。",
+ constraint: "需要启动资金时,请在配置列表使用“添加水位”,该操作会保留独立审计记录。",
+ },
+ loss_streak_guarantee: {
+ summary:
+ "同一用户在当前礼物奖池连续多少次返还为 0 后,下一次普通开奖会先去掉 0 倍档。填 5 表示连续 5 次为 0 后,第 6 次普通开奖尝试正奖。",
+ adjust: "调小会更快触发体验保护;调大会降低触发频率并减少奖池压力。",
+ constraint: "必须大于 0;奖池不足或任一返奖上限不足时仍可返回 0,不会透支。",
+ },
+ low_watermark_coins: {
+ summary: "本抽应进入奖池的金币先入账;入账后余额低于这个值时,系统会降低普通开奖中非 0 倍档的基础机会。",
+ adjust: "调高会更早开始保护奖池;调低会让正常中奖节奏保持更久。",
+ constraint: "必须大于 0,并且小于高余额分界线。",
+ },
+ low_water_nonzero_factor_percent: {
+ summary: "奖池处于低余额时,普通开奖中非 0 倍档的基础机会保留多少。填 70 表示按原来的 70% 参与计算。",
+ adjust: "调低更保护奖池但中奖更少;调高更接近正常节奏但消耗更快。",
+ constraint: "必须大于 0% 且小于 100%,0 倍档接收剩余概率;它不会直接提高或降低两种大奖的触发机会。",
+ },
+ high_watermark_coins: {
+ summary: "本抽应进入奖池的金币先入账;入账后余额高于这个值时,系统会提高普通开奖中非 0 倍档的基础机会。",
+ adjust: "调低会更早提高中奖机会;调高会要求奖池积累更多后才加速返奖。",
+ constraint: "必须严格大于低余额分界线;刚好等于该值时仍按正常状态处理。",
+ },
+ high_water_nonzero_factor_percent: {
+ summary: "奖池处于高余额时,普通开奖中非 0 倍档的基础机会放大到多少。填 130 表示变为原来的 130%。",
+ adjust: "调高会更快释放奖池、增加正奖机会;调低则更接近正常节奏。",
+ constraint: "必须大于 100%;放大后会重新归一化,并继续受奖池和返奖上限限制;它不会直接提高大奖触发机会。",
+ },
+ recharge_boost_window_ms: {
+ summary: "用户完成一次可验证充值后,普通开奖中非 0 倍档的机会加成持续多久。300000 毫秒等于 5 分钟。",
+ adjust: "调大会延长充值后的加成时间;调小会让加成更快结束。",
+ constraint: "必须大于 0,生效区间包含充值时刻但不包含结束时刻;外部接入没有可信充值事实,不会触发加成。",
+ },
+ recharge_boost_factor_percent: {
+ summary: "充值加成期间,普通开奖中非 0 倍档的基础机会放大到多少。填 110 表示变为原来的 110%。",
+ adjust: "调高会增强充值后的中奖体验,也会增加奖池消耗;调低则更温和。",
+ constraint: "必须大于 100%,会与高低余额调整相乘;它只改变普通奖档,不会直接提高大奖触发机会。",
+ },
+ jackpot_multipliers: {
+ summary: "列出大奖可能采用的倍数,例如 200、500、1000。实际奖励按本抽消费金额乘以倍数计算。",
+ adjust: "增加或调高倍数会放大奖励金额和资金压力;删除倍数会减少可选大奖。",
+ constraint: "用逗号分隔,必须都大于 1 且从小到大;系统会在所有付得起的大奖倍数中等机会选择一档。",
+ },
+ jackpot_global_rtp_max_percent: {
+ summary: "当前 App、奖池、规则版本在本轮窗口内的平均返还不高于这条线时,才满足补偿大奖的全局条件。",
+ adjust: "调高会让补偿大奖更容易进入候选;调低会要求全局返还更低才触发。",
+ constraint:
+ "必须大于 0% 且不超过 100%。全局、用户当日和近 72 小时三项都必须有消费样本并全部通过,才会直接选择可支付大奖。",
+ },
+ jackpot_user_day_rtp_max_percent: {
+ summary: "用户当天的平均返还不高于这条线时,才允许尝试补偿大奖。",
+ adjust: "调高会让更多用户满足条件;调低会把大奖更集中给当天返还明显偏低的用户。",
+ constraint: "按当前奖池的 UTC 自然日统计;没有消费样本时不触发。必须大于 0 且不能超过全局触发线。",
+ },
+ jackpot_user_72h_rtp_max_percent: {
+ summary: "用户最近连续 72 小时的平均返还不高于这条线时,才允许尝试补偿大奖。",
+ adjust: "调高会放宽长期条件;调低会要求用户近三天整体返还更低。",
+ constraint: "按当前奖池滚动 72 小时统计,不是三个本地自然日;没有消费样本时不触发,且不能超过全局触发线。",
+ },
+ max_jackpot_hits_per_user_day: {
+ summary: "限制同一用户在当前奖池的一个 UTC 自然日内最多真正拿到多少次大奖。",
+ adjust: "调高允许同一用户一天获得更多大奖;调低会更早停止该用户当天的大奖,不保证转给其他用户。",
+ constraint: "必须大于 0;达到次数后仍可参与普通奖档,尚未使用的消费资格会保留。",
+ },
+ jackpot_spend_threshold_coins: {
+ summary: "用户在当前奖池的同一 UTC 日内,每累计消费满一份门槛,就保存一个供后续抽奖使用的大奖资格。",
+ adjust: "按各 App 奖池确认的金币口径填写;调高会要求消费更多才获得资格,调低会更频繁地产生资格。",
+ constraint:
+ "启用前必须大于 0,系统不会根据币价或充值记录自行猜额度;一笔消费可产生多个资格,资格从下一抽开始用,未能支付时不会丢失。",
+ },
+ max_single_payout: {
+ summary: "限制一抽最多能返多少金币;超过这个金额的奖档当次不会发出。",
+ adjust: "调高允许更大的单次奖励,也增加资金风险;调低会直接挡住更多高倍率结果。",
+ constraint:
+ "启用 V3 时必须大于 0;批量送礼按每个子抽分别判断。奖档超过任一剩余额度会整档挡住,不会裁剪成较小金额。",
+ },
+ user_hourly_payout_cap: {
+ summary: "限制同一用户在当前 UTC 小时内累计最多返多少金币。",
+ adjust: "调高允许用户短时间获得更多奖励;调低会更早限制高频送礼用户。",
+ constraint: "启用 V3 时必须大于 0;按当前奖池和实际支付时间所在的 UTC 小时计算,到下一小时重新累计。",
+ },
+ user_daily_payout_cap: {
+ summary: "限制同一用户在当前 UTC 自然日内累计最多返多少金币。",
+ adjust: "调高允许单个用户一天获得更多奖励;调低会更均匀地分散预算。",
+ constraint: "启用 V3 时必须大于 0;按当前奖池和实际支付时间所在的 UTC 日期累计。",
+ },
+ device_daily_payout_cap: {
+ summary: "限制同一设备上的所有账号在一个 UTC 自然日内合计最多返多少金币,用来防止换账号绕过用户上限。",
+ adjust: "调高会放宽同设备多账号的总额度;调低会更严格地控制集中领取。",
+ constraint: "启用 V3 时必须大于 0,并按当前奖池隔离;必须拿到稳定设备 ID,外部接入也不能用用户 ID 代替。",
+ },
+ room_hourly_payout_cap: {
+ summary: "限制同一房间内所有用户在当前 UTC 小时合计最多返多少金币。",
+ adjust: "调高允许热门房间更集中地产生奖励;调低会更早限制单房间的消耗速度。",
+ constraint:
+ "启用 V3 时必须大于 0,并按当前奖池隔离;外部接口没有真实房间,同一 App 的外部请求会共用一个虚拟房间额度。",
+ },
+ anchor_daily_payout_cap: {
+ summary: "限制同一房主关联的所有房间和用户在一个 UTC 自然日内合计最多返多少金币。",
+ adjust: "调高允许奖励更集中在单个主播;调低会把预算分散到更多主播。",
+ constraint:
+ "启用 V3 时必须大于 0,并按当前奖池隔离;外部接口没有房主,同一 App 的外部请求会共用一个虚拟主播额度。",
+ },
+ stage_novice: {
+ summary:
+ "新手阶段使用的一组基础奖档。V2 看累计消费折算次数;V3 中未同时达到普通阶段两项充值门槛的用户都会落在这里。",
+ adjust: "可通过倍率组合调整未达到普通门槛用户的体验,但预计平均返还仍要落在允许范围内。",
+ constraint:
+ "V3 的新手充值门槛固定为 0 / 0;外部接入当前也始终按新手阶段处理。每个阶段都必须保留启用的 0 倍档。",
+ },
+ stage_normal: {
+ summary: "普通阶段使用的一组基础奖档。V2 看累计消费折算次数;V3 必须同时达到这里的 7 日和 30 日最低充值。",
+ adjust: "V3 中提高充值门槛会减少进入普通阶段的人数,降低则会扩大范围;V2 由上方等价抽数上限控制阶段。",
+ constraint: "V3 中至少一个充值门槛必须大于 0,并且不能高于高阶阶段对应门槛。",
+ },
+ stage_advanced: {
+ summary:
+ "高阶阶段使用的一组基础奖档。V2 超过普通折算次数上限后进入;V3 会选择同时满足 7 日和 30 日门槛的最高阶段。",
+ adjust: "V3 中提高充值门槛会缩小高阶范围,降低则会扩大;V2 由上方等价抽数上限控制阶段。",
+ constraint: "V3 中两项门槛都不能低于普通阶段,且至少一项必须更高。",
+ },
+ min_recharge_7d_coins: {
+ summary: "用户最近 7 个 UTC 自然日(含当天)的充值金币至少达到多少,才满足当前阶段的一半条件。",
+ adjust: "调高会减少进入该阶段的人数;调低会扩大范围。",
+ constraint: "用户还必须同时达到 30 日门槛;新手阶段固定为 0。外部接入没有可信充值快照时按新手处理。",
+ },
+ min_recharge_30d_coins: {
+ summary: "用户最近 30 个 UTC 自然日(含当天)的充值金币至少达到多少,才满足当前阶段的另一半条件。",
+ adjust: "调高会更看重长期充值;调低会让近期活跃用户更容易进入该阶段。",
+ constraint: "用户还必须同时达到 7 日门槛;新手阶段固定为 0。",
+ },
+ tier_multiplier: {
+ summary: "这一档返还本抽消费金额的多少倍。0 倍表示没有返还,1 倍表示返还与本抽消费相同。",
+ adjust: "调高会增加这一档的奖励金额;系统会自动重算各档基础概率来尽量保持平均返还目标。",
+ constraint: "不能小于 0;10 倍及以上必须勾选“高倍率安全标记”,实际支付仍不能超过奖池和返奖上限。",
+ },
+ tier_probability: {
+ summary: "表示在当前用户阶段中抽到这一档的基础机会,所有启用奖档必须合计 100%。",
+ adjust: "该值由平均返还目标和各档倍数自动计算,只读展示;修改倍数或目标后会重新计算。",
+ constraint: "V3 会按水位和可信充值调整普通非 0 档,并受六项上限限制;V2 保持基础概率,只过滤奖池付不起的档位。",
+ },
+ tier_high_water_only: {
+ summary: "这是发布高倍率基础奖档前的安全确认标记;当前运行逻辑不会根据它判断奖池处于哪种水位。",
+ adjust: "勾选或取消不会直接放行、拦截奖档;实际是否能发仍看实时奖池余额和返奖上限。",
+ constraint:
+ "10 倍及以上必须勾选才能发布。不要把它当成独立的中奖开关;V3 的高低余额变化由上方两组中奖权重控制。",
+ },
+ tier_enabled: {
+ summary: "决定这一档是否参加当前阶段的普通基础抽奖。关闭后不会从普通奖档中选中。",
+ adjust: "关闭高倍率档会降低大额结果种类;重新开启后系统会重新计算整组概率。",
+ constraint:
+ "每个阶段启用档位的概率必须合计 100%,并至少保留一个 0 倍档;若同倍率另列在大奖倍率中,大奖机制仍可能选中它。",
+ },
+};
+
+export function LuckyGiftFieldLabel({ field, label }) {
+ const help = FIELD_HELP[field];
+ if (!help) return label;
+ return (
+
}
+ >
+
+ {label}
+
+
+
+
+
+ );
+}
+
+function FieldHelpContent({ help, label }) {
+ return (
+
+
{label}
+
+
+
- 作用
+ - {help.summary}
+
+
+
- 调整影响
+ - {help.adjust}
+
+
+
- 注意
+ - {help.constraint}
+
+
+
+ );
+}
diff --git a/ops-center/src/components/LuckyGiftStageDesigner.jsx b/ops-center/src/components/LuckyGiftStageDesigner.jsx
index 65b6638..2878b14 100644
--- a/ops-center/src/components/LuckyGiftStageDesigner.jsx
+++ b/ops-center/src/components/LuckyGiftStageDesigner.jsx
@@ -6,6 +6,7 @@ import TextField from "@mui/material/TextField";
import { Button } from "@/shared/ui/Button.jsx";
import { formatDecimal } from "@/shared/utils/rtpProbability.js";
import { formatNumber } from "../format.js";
+import { LuckyGiftFieldLabel } from "./LuckyGiftFieldHelp.jsx";
import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
export function LuckyGiftStageDesigner({
@@ -58,10 +59,10 @@ export function LuckyGiftStageDesigner({
>
{summary.tabLabel}
- {summary.status}
+ {plainStageStatus(summary.status)}
- 概率 {formatDecimal(summary.probability)}% · RTP {formatDecimal(summary.expected)}%
+ 概率 {formatDecimal(summary.probability)}% · 平均返还 {formatDecimal(summary.expected)}%
);
@@ -77,9 +78,14 @@ export function LuckyGiftStageDesigner({
>
-
{activeSummary?.stageLabel}
+
+
+
@@ -96,9 +102,11 @@ export function LuckyGiftStageDesigner({
) : strategyVersion === "dynamic_v3" ? (
-
充值门槛
+
进入本阶段所需充值
+ }
size="small"
slotProps={{ htmlInput: { min: 0, step: 1 } }}
type="number"
@@ -112,7 +120,9 @@ export function LuckyGiftStageDesigner({
}
/>
+ }
size="small"
slotProps={{ htmlInput: { min: 0, step: 1 } }}
type="number"
@@ -130,10 +140,18 @@ export function LuckyGiftStageDesigner({
- 倍率
- 概率 %
- 高水位
- 启用
+
+
+
+
+
+
+
+
+
+
+
+
操作
{activeStageConfig.tiers.map((tier, index) => {
@@ -156,7 +174,8 @@ export function LuckyGiftStageDesigner({
value={tier.multiplier}
onChange={(event) =>
onTierChange(activeStageConfig.stage, index, {
- // 10 倍及以上强制只走高水位,输入时同步勾选并锁死,防止高倍档在低水位放开。
+ // 10 倍及以上是服务端发布校验要求的高倍率标记;运行时仍会独立检查真实奖池余额,
+ // 不能把前端勾选本身当成资金足够或必然可发奖的依据。
highWaterOnly:
Number(event.target.value) >= 10 || tier.highWaterOnly,
multiplier: event.target.value,
@@ -182,7 +201,7 @@ export function LuckyGiftStageDesigner({
checked={Boolean(tier.highWaterOnly)}
disabled={Number(tier.multiplier) >= 10}
size="small"
- slotProps={{ input: { "aria-label": `${rowLabel}高水位限制` } }}
+ slotProps={{ input: { "aria-label": `${rowLabel}高倍率安全标记` } }}
onChange={(event) =>
onTierChange(activeStageConfig.stage, index, {
highWaterOnly: event.target.checked,
@@ -246,3 +265,9 @@ function NoviceRechargeRange({ normalStageConfig }) {
);
}
+
+function plainStageStatus(status) {
+ if (status === "RTP 偏离") return "平均返还偏离";
+ if (status === "概率未闭合") return "概率合计不为 100%";
+ return status || "待检查";
+}
diff --git a/ops-center/src/styles/index.css b/ops-center/src/styles/index.css
index 9ba809e..b565b0c 100644
--- a/ops-center/src/styles/index.css
+++ b/ops-center/src/styles/index.css
@@ -445,6 +445,97 @@ body {
line-height: 1.5;
}
+.ops-field-label {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 4px;
+ border-radius: 3px;
+ cursor: help;
+ white-space: nowrap;
+}
+
+/* OutlinedInput 会在 notch 的 legend 中复制一次 label;保留其宽度计算,但隐藏复制品,
+ * 避免同一个说明入口在键盘 Tab 顺序中出现两次。 */
+.MuiOutlinedInput-notchedOutline legend .ops-field-label {
+ visibility: hidden;
+}
+
+.ops-field-help-trigger {
+ display: inline-flex;
+ width: 16px;
+ height: 16px;
+ flex: 0 0 16px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ color: var(--text-tertiary);
+ font-size: 14px;
+ transition:
+ background var(--motion-fast) var(--ease-standard),
+ color var(--motion-fast) var(--ease-standard);
+}
+
+.ops-field-label:hover .ops-field-help-trigger,
+.ops-field-label:focus-visible .ops-field-help-trigger {
+ background: var(--primary-hover);
+ color: var(--primary);
+}
+
+.ops-field-label:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--primary-border, #93c5fd);
+}
+
+/* Tooltip 通过 portal 渲染,必须用自身 class 承载完整排版;字段内只保留问号,不牺牲表单密度。 */
+.MuiTooltip-tooltip.ops-field-help-tooltip {
+ max-width: min(420px, calc(100vw - 32px));
+ border: 1px solid rgb(148 163 184 / 32%);
+ border-radius: var(--radius-control);
+ background: #0f172a;
+ box-shadow: 0 16px 40px rgb(15 23 42 / 28%);
+ color: #f8fafc;
+ padding: var(--space-3) var(--space-4);
+}
+
+.ops-field-help-content {
+ display: grid;
+ gap: var(--space-2);
+}
+
+.ops-field-help-content > strong {
+ color: #ffffff;
+ font-size: 13px;
+ line-height: 1.4;
+}
+
+.ops-field-help-content dl {
+ display: grid;
+ gap: 7px;
+ margin: 0;
+}
+
+.ops-field-help-content dl > div {
+ display: grid;
+ grid-template-columns: 56px minmax(0, 1fr);
+ align-items: start;
+ gap: var(--space-2);
+}
+
+.ops-field-help-content dt {
+ color: #93c5fd;
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 1.55;
+}
+
+.ops-field-help-content dd {
+ margin: 0;
+ color: #e2e8f0;
+ font-size: 12px;
+ line-height: 1.55;
+}
+
.ops-stage-designer {
display: grid;
min-width: 0;
@@ -620,8 +711,8 @@ body {
min-width: 590px;
grid-template-columns:
minmax(100px, 1fr)
- minmax(110px, 1fr)
- 76px
+ minmax(124px, 1fr)
+ 96px
64px
56px;
align-items: center;
@@ -642,6 +733,14 @@ body {
text-align: center;
}
+/* 奖档表头允许自然换行,说明图标不会在窄弹窗中挤到相邻列。 */
+.ops-tier-head .ops-field-label {
+ justify-content: center;
+ line-height: 1.2;
+ text-align: center;
+ white-space: normal;
+}
+
.ops-tier-row {
border-bottom: 1px solid var(--row-border);
padding: 6px var(--space-3);