fix(ops-center): clarify lucky gift thresholds
This commit is contained in:
parent
917a68b9d8
commit
43682534b0
@ -29,6 +29,7 @@ export function LuckyGiftConfigDialog({ appOptions = [], config, mode = "edit",
|
|||||||
: form.stages[0]?.stage;
|
: form.stages[0]?.stage;
|
||||||
const activeStageConfig = form.stages.find((stage) => stage.stage === activeStageKey);
|
const activeStageConfig = form.stages.find((stage) => stage.stage === activeStageKey);
|
||||||
const activeSummary = stageSummaries.find((summary) => summary.stageKey === activeStageKey);
|
const activeSummary = stageSummaries.find((summary) => summary.stageKey === activeStageKey);
|
||||||
|
const normalStageConfig = form.stages.find((stage) => stage.stage === "normal");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!validationErrors.length) return;
|
if (!validationErrors.length) return;
|
||||||
@ -192,6 +193,7 @@ export function LuckyGiftConfigDialog({ appOptions = [], config, mode = "edit",
|
|||||||
activeStageConfig={activeStageConfig}
|
activeStageConfig={activeStageConfig}
|
||||||
activeStageKey={activeStageKey}
|
activeStageKey={activeStageKey}
|
||||||
activeSummary={activeSummary}
|
activeSummary={activeSummary}
|
||||||
|
normalStageConfig={normalStageConfig}
|
||||||
stageSummaries={stageSummaries}
|
stageSummaries={stageSummaries}
|
||||||
strategyVersion={form.strategy_version}
|
strategyVersion={form.strategy_version}
|
||||||
onAddTier={addStageTier}
|
onAddTier={addStageTier}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ test("keeps every dynamic_v3 control reachable while locking rule-time pool inje
|
|||||||
"充值加权窗口 (ms)",
|
"充值加权窗口 (ms)",
|
||||||
"大奖倍率",
|
"大奖倍率",
|
||||||
"全局大奖 RTP 上限 (%)",
|
"全局大奖 RTP 上限 (%)",
|
||||||
|
"用户日累计消费触发门槛(金币)",
|
||||||
"单次返奖上限",
|
"单次返奖上限",
|
||||||
"主播每日上限",
|
"主播每日上限",
|
||||||
].forEach((label) =>
|
].forEach((label) =>
|
||||||
@ -35,6 +36,14 @@ test("keeps every dynamic_v3 control reachable while locking rule-time pool inje
|
|||||||
expect(within(dialog).getByLabelText("新手阶段第 1 档倍率")).toBeInTheDocument();
|
expect(within(dialog).getByLabelText("新手阶段第 1 档倍率")).toBeInTheDocument();
|
||||||
expect(within(dialog).getByLabelText("新手阶段第 2 档概率 %")).toHaveAttribute("readonly");
|
expect(within(dialog).getByLabelText("新手阶段第 2 档概率 %")).toHaveAttribute("readonly");
|
||||||
expect(within(dialog).getByRole("button", { name: "删除新手阶段第 1 档" })).toBeInTheDocument();
|
expect(within(dialog).getByRole("button", { name: "删除新手阶段第 1 档" })).toBeInTheDocument();
|
||||||
|
expect(within(dialog).getByLabelText("新手阶段派生充值范围")).toHaveTextContent("7 日充值 < 0 或 30 日充值 < 1");
|
||||||
|
expect(
|
||||||
|
within(dialog).getByText("任一未达到即为新手;7 日与 30 日均达到门槛(含等于)后进入普通。"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByRole("tab", { name: /普通/ }));
|
||||||
|
expect(within(dialog).getByLabelText("7 日最低充值(金币)")).toHaveValue(0);
|
||||||
|
expect(within(dialog).getByLabelText("30 日最低充值(金币)")).toHaveValue(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("section navigation handles reduced motion and falls back after switching from dynamic_v3 to fixed_v2", async () => {
|
test("section navigation handles reduced motion and falls back after switching from dynamic_v3 to fixed_v2", async () => {
|
||||||
|
|||||||
@ -375,7 +375,8 @@ export function LuckyGiftConfigSections({ appOptions, form, isCreate, onChange }
|
|||||||
/>
|
/>
|
||||||
<NumberField
|
<NumberField
|
||||||
form={form}
|
form={form}
|
||||||
label="50 美元等值消费门槛"
|
helperText="按应用奖池规则配置"
|
||||||
|
label="用户日累计消费触发门槛(金币)"
|
||||||
name="jackpot_spend_threshold_coins"
|
name="jackpot_spend_threshold_coins"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { formatDecimal } from "@/shared/utils/rtpProbability.js";
|
import { formatDecimal } from "@/shared/utils/rtpProbability.js";
|
||||||
|
import { formatNumber } from "../format.js";
|
||||||
import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
|
import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
|
||||||
|
|
||||||
export function LuckyGiftStageDesigner({
|
export function LuckyGiftStageDesigner({
|
||||||
activeStageConfig,
|
activeStageConfig,
|
||||||
activeStageKey,
|
activeStageKey,
|
||||||
activeSummary,
|
activeSummary,
|
||||||
|
normalStageConfig,
|
||||||
onAddTier,
|
onAddTier,
|
||||||
onRemoveTier,
|
onRemoveTier,
|
||||||
onStageChange,
|
onStageChange,
|
||||||
@ -90,12 +92,13 @@ export function LuckyGiftStageDesigner({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{strategyVersion === "dynamic_v3" ? (
|
{strategyVersion === "dynamic_v3" && activeStageConfig.stage === "novice" ? (
|
||||||
|
<NoviceRechargeRange normalStageConfig={normalStageConfig} />
|
||||||
|
) : strategyVersion === "dynamic_v3" ? (
|
||||||
<div className="ops-stage-thresholds" aria-label={`${activeSummary?.stageLabel}充值门槛`}>
|
<div className="ops-stage-thresholds" aria-label={`${activeSummary?.stageLabel}充值门槛`}>
|
||||||
<span>充值门槛</span>
|
<span>充值门槛</span>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={activeStageConfig.stage === "novice"}
|
label="7 日最低充值(金币)"
|
||||||
label="7 日充值"
|
|
||||||
size="small"
|
size="small"
|
||||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||||
type="number"
|
type="number"
|
||||||
@ -109,8 +112,7 @@ export function LuckyGiftStageDesigner({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={activeStageConfig.stage === "novice"}
|
label="30 日最低充值(金币)"
|
||||||
label="30 日充值"
|
|
||||||
size="small"
|
size="small"
|
||||||
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
slotProps={{ htmlInput: { min: 0, step: 1 } }}
|
||||||
type="number"
|
type="number"
|
||||||
@ -221,3 +223,26 @@ export function LuckyGiftStageDesigner({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NoviceRechargeRange({ normalStageConfig }) {
|
||||||
|
const normal7D = Number(normalStageConfig?.min_recharge_7d_coins || 0);
|
||||||
|
const normal30D = Number(normalStageConfig?.min_recharge_30d_coins || 0);
|
||||||
|
// normal 是二维 AND 下限,合法配置只需至少一维大于 0;为 0 的维度也必须照实展示。
|
||||||
|
const configured = Number.isFinite(normal7D) && Number.isFinite(normal30D) && (normal7D > 0 || normal30D > 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ops-stage-thresholds ops-stage-thresholds--derived" aria-label="新手阶段派生充值范围">
|
||||||
|
<span>派生范围</span>
|
||||||
|
<div className="ops-stage-threshold-rule">
|
||||||
|
<strong>
|
||||||
|
7 日充值 < {formatNumber(normal7D)} 或 30 日充值 < {formatNumber(normal30D)}
|
||||||
|
</strong>
|
||||||
|
{/* 普通阶段需要两个下限同时成立;因此任一维未达到都应留在新手,等于下限则进入普通。 */}
|
||||||
|
<small>
|
||||||
|
{configured ? "" : "普通阶段门槛待配置;"}任一未达到即为新手;7 日与 30
|
||||||
|
日均达到门槛(含等于)后进入普通。
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { DEFAULT_POOL_ID } from "./api.js";
|
|||||||
const DYNAMIC_STRATEGY = "dynamic_v3";
|
const DYNAMIC_STRATEGY = "dynamic_v3";
|
||||||
const PPM_SCALE = 1_000_000;
|
const PPM_SCALE = 1_000_000;
|
||||||
|
|
||||||
// 只有跨 App 通用的算法参数才有默认值;50 美元等值门槛和六维金额上限必须由运营按 App 金币口径填写,
|
// 只有跨 App 通用的算法参数才有默认值;用户日累计消费触发门槛和六维金额上限必须由运营按 App 奖池的金币口径填写,
|
||||||
// 因而默认保持 0 且规则停用,避免把某个 App 的预算误发到其他 App。
|
// 因而默认保持 0 且规则停用,避免把某个 App 的预算误发到其他 App。
|
||||||
const dynamicDefaults = {
|
const dynamicDefaults = {
|
||||||
anchor_daily_payout_cap: 0,
|
anchor_daily_payout_cap: 0,
|
||||||
@ -131,18 +131,23 @@ export function formToConfig(config, form) {
|
|||||||
enabled: Boolean(form.enabled),
|
enabled: Boolean(form.enabled),
|
||||||
jackpot_multipliers: multiplierListFromForm(form.jackpot_multipliers),
|
jackpot_multipliers: multiplierListFromForm(form.jackpot_multipliers),
|
||||||
pool_id: form.pool_id,
|
pool_id: form.pool_id,
|
||||||
stages: (form.stages || []).map((stage) => ({
|
stages: (form.stages || []).map((stage) => {
|
||||||
min_recharge_30d_coins: numberFromForm(stage.min_recharge_30d_coins),
|
// dynamic_v3 的新手范围由普通阶段下限反向派生;0/0 只是旧 DTO 要求的兜底哨兵值,
|
||||||
min_recharge_7d_coins: numberFromForm(stage.min_recharge_7d_coins),
|
// 不是可运营的“充值为 0 才是新手”规则。序列化时强制归零,避免历史异常值改变分层。
|
||||||
stage: stage.stage,
|
const isDynamicNovice = form.strategy_version === DYNAMIC_STRATEGY && stage.stage === "novice";
|
||||||
tiers: (stage.tiers || []).map((tier) => ({
|
return {
|
||||||
enabled: tier.enabled !== false,
|
min_recharge_30d_coins: isDynamicNovice ? 0 : numberFromForm(stage.min_recharge_30d_coins),
|
||||||
high_water_only: Boolean(tier.highWaterOnly),
|
min_recharge_7d_coins: isDynamicNovice ? 0 : numberFromForm(stage.min_recharge_7d_coins),
|
||||||
multiplier: numberFromForm(tier.multiplier),
|
stage: stage.stage,
|
||||||
probability_percent: numberFromForm(tier.probabilityPercent),
|
tiers: (stage.tiers || []).map((tier) => ({
|
||||||
tier_id: tier.tierId || "",
|
enabled: tier.enabled !== false,
|
||||||
})),
|
high_water_only: Boolean(tier.highWaterOnly),
|
||||||
})),
|
multiplier: numberFromForm(tier.multiplier),
|
||||||
|
probability_percent: numberFromForm(tier.probabilityPercent),
|
||||||
|
tier_id: tier.tierId || "",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
strategy_version: form.strategy_version,
|
strategy_version: form.strategy_version,
|
||||||
};
|
};
|
||||||
for (const field of numericFields) {
|
for (const field of numericFields) {
|
||||||
@ -316,9 +321,9 @@ export function validateLuckyGiftForm(form) {
|
|||||||
for (const [field, label] of payoutCapFields) {
|
for (const [field, label] of payoutCapFields) {
|
||||||
if (number(field) < 0) errors.push(`${label}不能小于 0`);
|
if (number(field) < 0) errors.push(`${label}不能小于 0`);
|
||||||
}
|
}
|
||||||
validateRechargeStages(form.stages || [], errors);
|
validateRechargeStages(form.stages || [], errors, form.enabled);
|
||||||
if (form.enabled) {
|
if (form.enabled) {
|
||||||
if (number("jackpot_spend_threshold_coins") <= 0) errors.push("启用前必须填写 50 美元等值的大奖消费门槛");
|
if (number("jackpot_spend_threshold_coins") <= 0) errors.push("启用前必须填写用户日累计消费触发门槛(金币)");
|
||||||
for (const [field, label] of payoutCapFields) {
|
for (const [field, label] of payoutCapFields) {
|
||||||
if (number(field) <= 0) errors.push(`启用前必须填写${label}`);
|
if (number(field) <= 0) errors.push(`启用前必须填写${label}`);
|
||||||
}
|
}
|
||||||
@ -345,11 +350,15 @@ function normalizeFormStages(stages, targetRTPPercent, strategyVersion) {
|
|||||||
: correctedDefaultTiers(stageKey, targetRTPPercent);
|
: correctedDefaultTiers(stageKey, targetRTPPercent);
|
||||||
const [default7d, default30d] =
|
const [default7d, default30d] =
|
||||||
strategyVersion === DYNAMIC_STRATEGY ? defaultRechargeThreshold(stageKey) : [0, 0];
|
strategyVersion === DYNAMIC_STRATEGY ? defaultRechargeThreshold(stageKey) : [0, 0];
|
||||||
|
const isDynamicNovice = strategyVersion === DYNAMIC_STRATEGY && stageKey === "novice";
|
||||||
return {
|
return {
|
||||||
min_recharge_7d_coins: formNumber(source?.min_recharge_7d_coins ?? source?.minRecharge7DCoins ?? default7d),
|
// 只在表单内保留兼容哨兵值;界面上的新手上界始终从普通门槛派生,不读取历史 novice 门槛。
|
||||||
min_recharge_30d_coins: formNumber(
|
min_recharge_7d_coins: isDynamicNovice
|
||||||
source?.min_recharge_30d_coins ?? source?.minRecharge30DCoins ?? default30d,
|
? "0"
|
||||||
),
|
: formNumber(source?.min_recharge_7d_coins ?? source?.minRecharge7DCoins ?? default7d),
|
||||||
|
min_recharge_30d_coins: isDynamicNovice
|
||||||
|
? "0"
|
||||||
|
: formNumber(source?.min_recharge_30d_coins ?? source?.minRecharge30DCoins ?? default30d),
|
||||||
stage: stageKey,
|
stage: stageKey,
|
||||||
tiers: sourceTiers.map((tier) => ({
|
tiers: sourceTiers.map((tier) => ({
|
||||||
enabled: tier.enabled !== false,
|
enabled: tier.enabled !== false,
|
||||||
@ -382,13 +391,10 @@ function defaultTier(tierID, multiplier, probabilityPercent) {
|
|||||||
return { enabled: true, highWaterOnly: false, multiplier, probabilityPercent, tierId: tierID };
|
return { enabled: true, highWaterOnly: false, multiplier, probabilityPercent, tierId: tierID };
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRechargeStages(stages, errors) {
|
function validateRechargeStages(stages, errors, requireComplete) {
|
||||||
const novice = stages.find((stage) => stage.stage === "novice");
|
|
||||||
const normal = stages.find((stage) => stage.stage === "normal");
|
const normal = stages.find((stage) => stage.stage === "normal");
|
||||||
const advanced = stages.find((stage) => stage.stage === "advanced");
|
const advanced = stages.find((stage) => stage.stage === "advanced");
|
||||||
const threshold = (stage, field) => numberFromForm(stage?.[field]);
|
const threshold = (stage, field) => numberFromForm(stage?.[field]);
|
||||||
if (threshold(novice, "min_recharge_7d_coins") !== 0 || threshold(novice, "min_recharge_30d_coins") !== 0)
|
|
||||||
errors.push("新手阶段充值门槛必须为 0 / 0");
|
|
||||||
const normal7d = threshold(normal, "min_recharge_7d_coins");
|
const normal7d = threshold(normal, "min_recharge_7d_coins");
|
||||||
const normal30d = threshold(normal, "min_recharge_30d_coins");
|
const normal30d = threshold(normal, "min_recharge_30d_coins");
|
||||||
const advanced7d = threshold(advanced, "min_recharge_7d_coins");
|
const advanced7d = threshold(advanced, "min_recharge_7d_coins");
|
||||||
@ -397,15 +403,21 @@ function validateRechargeStages(stages, errors) {
|
|||||||
errors.push("充值阶段门槛不能小于 0");
|
errors.push("充值阶段门槛不能小于 0");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (normal7d === 0 && normal30d === 0) errors.push("正常阶段至少一个充值门槛必须大于 0");
|
const isUnconfigured = [normal7d, normal30d, advanced7d, advanced30d].every((value) => value === 0);
|
||||||
|
// 新建的停用草稿可以全 0,避免猜测 App 金币价值;一旦开始填写或准备启用,
|
||||||
|
// 就必须遵守 owner 的二维下限契约:normal 至少一维大于 0,advanced 逐维不低且至少一维更高。
|
||||||
|
if (!requireComplete && isUnconfigured) return;
|
||||||
|
if (normal7d === 0 && normal30d === 0)
|
||||||
|
errors.push(
|
||||||
|
requireComplete ? "启用前普通阶段至少一个最低充值门槛必须大于 0" : "普通阶段至少一个充值门槛必须大于 0",
|
||||||
|
);
|
||||||
if (advanced7d < normal7d || advanced30d < normal30d || (advanced7d === normal7d && advanced30d === normal30d))
|
if (advanced7d < normal7d || advanced30d < normal30d || (advanced7d === normal7d && advanced30d === normal30d))
|
||||||
errors.push("高阶充值门槛必须逐维不低于正常阶段且至少一项更高");
|
errors.push("高阶充值门槛必须逐维不低于正常阶段且至少一项更高");
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultRechargeThreshold(stage) {
|
function defaultRechargeThreshold() {
|
||||||
if (stage === "novice") return [0, 0];
|
// 充值门槛受 App 金币比例影响,新草稿不猜测 1 金币等于可用配置;运营填完后才能启用。
|
||||||
if (stage === "normal") return [0, 1];
|
return [0, 0];
|
||||||
return [1, 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultNumber(field, strategyVersion) {
|
function defaultNumber(field, strategyVersion) {
|
||||||
|
|||||||
@ -18,8 +18,8 @@ describe("ops center lucky gift dynamic_v3 form", () => {
|
|||||||
});
|
});
|
||||||
expect(form.stages.map((stage) => [stage.min_recharge_7d_coins, stage.min_recharge_30d_coins])).toEqual([
|
expect(form.stages.map((stage) => [stage.min_recharge_7d_coins, stage.min_recharge_30d_coins])).toEqual([
|
||||||
["0", "0"],
|
["0", "0"],
|
||||||
["0", "1"],
|
["0", "0"],
|
||||||
["1", "1"],
|
["0", "0"],
|
||||||
]);
|
]);
|
||||||
expect(form.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([
|
expect(form.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([
|
||||||
[5, 4, 86, 5],
|
[5, 4, 86, 5],
|
||||||
@ -52,10 +52,15 @@ describe("ops center lucky gift dynamic_v3 form", () => {
|
|||||||
test("drops legacy dynamic initial seed when loading and publishing a new version", () => {
|
test("drops legacy dynamic initial seed when loading and publishing a new version", () => {
|
||||||
const legacy = completeDynamicConfig();
|
const legacy = completeDynamicConfig();
|
||||||
legacy.initial_pool_coins = 1_000_000;
|
legacy.initial_pool_coins = 1_000_000;
|
||||||
|
legacy.stages[0].min_recharge_7d_coins = 12_345;
|
||||||
|
legacy.stages[0].min_recharge_30d_coins = 67_890;
|
||||||
|
|
||||||
const form = configToForm(legacy);
|
const form = configToForm(legacy);
|
||||||
expect(form.initial_pool_coins).toBe("0");
|
expect(form.initial_pool_coins).toBe("0");
|
||||||
expect(formToConfig(legacy, form).initial_pool_coins).toBe(0);
|
expect(form.stages[0]).toMatchObject({ min_recharge_7d_coins: "0", min_recharge_30d_coins: "0" });
|
||||||
|
const output = formToConfig(legacy, form);
|
||||||
|
expect(output.initial_pool_coins).toBe(0);
|
||||||
|
expect(output.stages[0]).toMatchObject({ min_recharge_7d_coins: 0, min_recharge_30d_coins: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allows disabled drafts but blocks enabled dynamic rules until app monetary limits are filled", () => {
|
test("allows disabled drafts but blocks enabled dynamic rules until app monetary limits are filled", () => {
|
||||||
@ -65,7 +70,9 @@ describe("ops center lucky gift dynamic_v3 form", () => {
|
|||||||
const enabled = { ...draft, enabled: true };
|
const enabled = { ...draft, enabled: true };
|
||||||
const errors = validateLuckyGiftForm(enabled);
|
const errors = validateLuckyGiftForm(enabled);
|
||||||
|
|
||||||
expect(errors).toContain("启用前必须填写 50 美元等值的大奖消费门槛");
|
expect(errors).toContain("启用前必须填写用户日累计消费触发门槛(金币)");
|
||||||
|
expect(errors).toContain("启用前普通阶段至少一个最低充值门槛必须大于 0");
|
||||||
|
expect(errors).toContain("高阶充值门槛必须逐维不低于正常阶段且至少一项更高");
|
||||||
expect(errors).toContain("启用前必须填写单次返奖上限");
|
expect(errors).toContain("启用前必须填写单次返奖上限");
|
||||||
expect(errors).toContain("启用前必须填写主播每日上限");
|
expect(errors).toContain("启用前必须填写主播每日上限");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -583,6 +583,27 @@ body {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-stage-threshold-rule {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
background: var(--bg-card-strong, #f8fafc);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-stage-threshold-rule strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-stage-threshold-rule small {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-tier-table {
|
.ops-tier-table {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -812,6 +833,10 @@ body {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-stage-threshold-rule {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
/* 小屏保留完整列头并让表格横向滚动,控件同时有阶段+行号标签,避免一列卡片失去字段关系。 */
|
/* 小屏保留完整列头并让表格横向滚动,控件同时有阶段+行号标签,避免一列卡片失去字段关系。 */
|
||||||
.ops-tier-head,
|
.ops-tier-head,
|
||||||
.ops-tier-row {
|
.ops-tier-row {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user