Restructure Plan UI for semi-auto tabs and T-quote ladder.

Hide amplitude card when filter off; compact semi form with rules; monitor tab keeps positions and market.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 14:01:12 +08:00
parent 457838ef64
commit d98bf46549
4 changed files with 632 additions and 189 deletions
+14 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Query
from ..market import get_gateway from ..market import get_gateway
from .auth import require_user from .auth import require_user
@@ -18,6 +18,19 @@ async def market_snapshot(_user: Annotated[str, Depends(require_user)]) -> dict:
return snap return snap
@router.get("/option-ladder")
async def market_option_ladder(
_user: Annotated[str, Depends(require_user)],
wings: int = Query(default=4, ge=1, le=12),
) -> dict:
"""半自动页 T 型报价:ATM 上下各 wings 档。"""
gw = get_gateway()
ladder = getattr(gw, "option_ladder", None)
if not callable(ladder):
raise HTTPException(status_code=501, detail="当前会话不支持 option-ladder")
return ladder(wings=wings)
@router.post("/realign") @router.post("/realign")
async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict: async def market_realign(_user: Annotated[str, Depends(require_user)]) -> dict:
"""手动重对齐次日到期 ATM 合约(运维/调试用)。""" """手动重对齐次日到期 ATM 合约(运维/调试用)。"""
+93
View File
@@ -1058,6 +1058,99 @@ class StrategySession:
d["ask_compare"] = ac d["ask_compare"] = ac
return d return d
def option_ladder(self, *, wings: int = 4) -> dict[str, Any]:
"""
T 型报价:当前监控到期附近若干档 Call/Put 盘口。
行:虚值 Call(上行)/ ATM / 虚值 Put(下行);列:Call 卖一·杠杆 | 行权价 | Put 卖一·杠杆。
"""
s = self.settings
wings = max(1, min(12, int(wings)))
idx = self.ex.fetch_index(s.index_inst_id)
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
if mark is None or float(mark) <= 0:
return {"ok": False, "detail": "无标的价", "rows": [], "index_px": None}
underlying = float(mark)
contracts = self.ex.list_option_contracts(s.option_inst_family)
from .selection import _complete_by_expiry, pick_atm_strike
eligible = list_eligible_expiry_ymds(contracts, min_hours=1.0)
ymd = None
if self._pair is not None and self._pair.expiry_ymd:
ymd = str(self._pair.expiry_ymd)
if not ymd and eligible:
ymd = eligible[0]
if not ymd:
return {
"ok": False,
"detail": "无合格到期",
"rows": [],
"index_px": underlying,
}
complete = _complete_by_expiry(contracts)
if ymd not in complete:
return {
"ok": False,
"detail": f"到期 {ymd} 无完整对",
"rows": [],
"index_px": underlying,
"expiry_ymd": ymd,
}
_ems, strikes_map = complete[ymd]
strikes = sorted(float(k) for k in strikes_map.keys())
atm = pick_atm_strike(strikes, underlying)
if atm is None:
return {
"ok": False,
"detail": "无 ATM",
"rows": [],
"index_px": underlying,
"expiry_ymd": ymd,
}
atm_i = min(range(len(strikes)), key=lambda i: abs(strikes[i] - float(atm)))
lo = max(0, atm_i - wings)
hi = min(len(strikes), atm_i + wings + 1)
rows: list[dict[str, Any]] = []
for k in strikes[lo:hi]:
legs = strikes_map[k]
call_id = legs.get("C")
put_id = legs.get("P")
cq = self.ex.quote(str(call_id)) if call_id else None
pq = self.ex.quote(str(put_id)) if put_id else None
c_ask = float(cq.ask) if cq and cq.ask is not None else None
p_ask = float(pq.ask) if pq and pq.ask is not None else None
c_bid = float(cq.bid) if cq and cq.bid is not None else None
p_bid = float(pq.bid) if pq and pq.bid is not None else None
off = float(k) - underlying
if abs(float(k) - float(atm)) < 1e-9:
tag = "atm"
elif float(k) > underlying + 1e-9:
tag = "otm_call" # Call 虚值 / Put 实值
else:
tag = "otm_put" # Put 虚值 / Call 实值
rows.append(
{
"strike": float(k),
"offset": round(off, 2),
"tag": tag,
"call_ask": c_ask,
"call_bid": c_bid,
"call_lev": option_leverage(underlying, c_ask),
"put_ask": p_ask,
"put_bid": p_bid,
"put_lev": option_leverage(underlying, p_ask),
"call_inst_id": call_id,
"put_inst_id": put_id,
}
)
return {
"ok": True,
"detail": "",
"index_px": underlying,
"expiry_ymd": ymd,
"atm_strike": float(atm),
"rows": rows,
}
async def _refresh_loop(self) -> None: async def _refresh_loop(self) -> None:
while True: while True:
await asyncio.sleep(30 if self._pair is not None else 10) await asyncio.sleep(30 if self._pair is not None else 10)
+387 -188
View File
@@ -122,6 +122,23 @@ export default function PlanPage() {
const [semiPerpU, setSemiPerpU] = useState(1); const [semiPerpU, setSemiPerpU] = useState(1);
const [semiOptU, setSemiOptU] = useState(4); const [semiOptU, setSemiOptU] = useState(4);
const [semiDirty, setSemiDirty] = useState(false); const [semiDirty, setSemiDirty] = useState(false);
const [planTab, setPlanTab] = useState<"semi" | "monitor">("semi");
const [ladder, setLadder] = useState<{
ok?: boolean;
detail?: string;
index_px?: number | null;
expiry_ymd?: string;
atm_strike?: number;
rows?: Array<{
strike: number;
offset: number;
tag: string;
call_ask: number | null;
call_lev: number | null;
put_ask: number | null;
put_lev: number | null;
}>;
} | null>(null);
const semiParamsBody = () => ({ const semiParamsBody = () => ({
semi_view_side: semiView, semi_view_side: semiView,
@@ -149,12 +166,26 @@ export default function PlanPage() {
setSemiExitU(Number(p.semi_perp_exit_unit ?? 5)); setSemiExitU(Number(p.semi_perp_exit_unit ?? 5));
setSemiMinH(Number(p.semi_min_option_hours ?? 30)); setSemiMinH(Number(p.semi_min_option_hours ?? 30));
setSemiMinLev(Number(p.semi_min_option_leverage ?? 200)); setSemiMinLev(Number(p.semi_min_option_leverage ?? 200));
const m = p.semi_moneyness; const mny = p.semi_moneyness;
setSemiMny(m === "itm" || m === "atm" || m === "otm" ? m : "otm"); setSemiMny(mny === "itm" || mny === "atm" || mny === "otm" ? mny : "otm");
setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25)); setSemiOtmOff(Number(p.semi_otm_max_offset ?? 25));
setSemiPerpU(Number(p.semi_perp_unit ?? 1)); setSemiPerpU(Number(p.semi_perp_unit ?? 1));
setSemiOptU(Number(p.semi_option_unit ?? 4)); setSemiOptU(Number(p.semi_option_unit ?? 4));
} }
if (
p.semi_auto_enabled &&
String(p.hedge_mode || "") !== "option_option"
) {
try {
setLadder(
await apiFetch<NonNullable<typeof ladder>>(
"/api/market/option-ladder?wings=4",
),
);
} catch {
/* ignore ladder errors */
}
}
setErr(""); setErr("");
} catch (e) { } catch (e) {
setErr(e instanceof Error ? e.message : String(e)); setErr(e instanceof Error ? e.message : String(e));
@@ -276,6 +307,8 @@ export default function PlanPage() {
snap?.hedge_mode === "option_option" || snap?.hedge_mode === "option_option" ||
pos?.hedge_mode === "option_option" || pos?.hedge_mode === "option_option" ||
!!pos?.option2_inst_id; !!pos?.option2_inst_id;
const semiOn = !!plan?.semi_auto_enabled && !isOo;
const showAmpCard = plan?.oo_amplitude_filter_enabled === true;
const exitMode = plan?.exit_mode ?? "fixed_usdt"; const exitMode = plan?.exit_mode ?? "fixed_usdt";
const riskBased = const riskBased =
plan?.risk_based === true || plan?.sizing_mode === "risk_based"; plan?.risk_based === true || plan?.sizing_mode === "risk_based";
@@ -573,209 +606,351 @@ export default function PlanPage() {
{busy ? <span className="meta">{busy}</span> : null} {busy ? <span className="meta">{busy}</span> : null}
</div> </div>
{plan?.semi_auto_enabled && !isOo ? ( {semiOn ? (
<section className="card plan-semi-card" aria-label="半自动本单"> <div className="tabs plan-main-tabs" role="tablist" aria-label="计划页">
<h3 className="plan-panel-title"> · </h3> <button
<p className="meta" style={{ marginTop: 0 }}> type="button"
// 180200±&gt;0×k role="tab"
</p> className={planTab === "semi" ? "tab active" : "tab"}
<div className="settings-fields" style={{ marginTop: 8 }}> aria-selected={planTab === "semi"}
<div className="field"> onClick={() => setPlanTab("semi")}
<label htmlFor="semiView"></label> >
<select
id="semiView" </button>
className="mono" <button
disabled={open || !!busy || !!plan.semi_armed} type="button"
value={semiView} role="tab"
onChange={(e) => { className={planTab === "monitor" ? "tab active" : "tab"}
setSemiView(e.target.value === "short" ? "short" : "long"); aria-selected={planTab === "monitor"}
setSemiDirty(true); onClick={() => setPlanTab("monitor")}
}} >
>
<option value="long"> · Call + </option> </button>
<option value="short"> · Put + </option> </div>
</select> ) : null}
{semiOn && planTab === "semi" ? (
<div className="plan-semi-tab">
<section className="card plan-semi-card plan-semi-compact" aria-label="半自动本单">
<div className="plan-semi-head">
<h3 className="plan-panel-title"> · </h3>
<span className="mono meta">
{plan.semi_armed ? "已授权" : "未授权"}
{" · "}
{semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值"}
{" · "}
{fmt(semiPerpU, 0)}:{fmt(semiOptU, 0)}
</span>
</div> </div>
<div className="field"> <details className="plan-semi-rules">
<label htmlFor="semiMny"></label> <summary></summary>
<select <ul>
id="semiMny" <li>永续:期权配比与出场 //</li>
className="mono" <li></li>
disabled={open || !!busy || !!plan.semi_armed} <li> 180 200/</li>
value={semiMny} <li> ± &gt; 0 </li>
onChange={(e) => { <li> × k </li>
const v = e.target.value; <li> k</li>
setSemiMny( </ul>
v === "itm" || v === "atm" || v === "otm" ? v : "otm", </details>
); <div className="settings-fields plan-semi-fields">
setSemiDirty(true);
}}
>
<option value="itm"></option>
<option value="atm"></option>
<option value="otm"></option>
</select>
</div>
{semiMny === "otm" ? (
<div className="field"> <div className="field">
<label htmlFor="semiOtmOff"></label> <label htmlFor="semiView"></label>
<select
id="semiView"
className="mono"
disabled={open || !!busy || !!plan.semi_armed}
value={semiView}
onChange={(e) => {
setSemiView(e.target.value === "short" ? "short" : "long");
setSemiDirty(true);
}}
>
<option value="long"> · Call+</option>
<option value="short"> · Put+</option>
</select>
</div>
<div className="field">
<label htmlFor="semiMny"></label>
<select
id="semiMny"
className="mono"
disabled={open || !!busy || !!plan.semi_armed}
value={semiMny}
onChange={(e) => {
const v = e.target.value;
setSemiMny(
v === "itm" || v === "atm" || v === "otm" ? v : "otm",
);
setSemiDirty(true);
}}
>
<option value="itm"></option>
<option value="atm"></option>
<option value="otm"></option>
</select>
</div>
{semiMny === "otm" ? (
<div className="field">
<label htmlFor="semiOtmOff"></label>
<input
id="semiOtmOff"
className="mono"
type="number"
step="1"
min="1"
disabled={open || !!busy || !!plan.semi_armed}
value={semiOtmOff}
onChange={(e) => {
setSemiOtmOff(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
) : null}
<div className="field">
<label htmlFor="semiPerpU"></label>
<input <input
id="semiOtmOff" id="semiPerpU"
className="mono"
type="number"
step="0.1"
min="0.01"
disabled={open || !!busy || !!plan.semi_armed}
value={semiPerpU}
onChange={(e) => {
setSemiPerpU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiOptU"></label>
<input
id="semiOptU"
className="mono"
type="number"
step="0.1"
min="0.01"
disabled={open || !!busy || !!plan.semi_armed}
value={semiOptU}
onChange={(e) => {
setSemiOptU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiMove"></label>
<input
id="semiMove"
className="mono" className="mono"
type="number" type="number"
step="1" step="1"
min="1" min="1"
disabled={open || !!busy || !!plan.semi_armed} disabled={open || !!busy || !!plan.semi_armed}
value={semiOtmOff} value={semiMove}
onChange={(e) => { onChange={(e) => {
setSemiOtmOff(Number(e.target.value)); setSemiMove(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiExitU"></label>
<input
id="semiExitU"
className="mono"
type="number"
step="0.1"
min="0.1"
disabled={open || !!busy || !!plan.semi_armed}
value={semiExitU}
onChange={(e) => {
setSemiExitU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiMinH"> h</label>
<input
id="semiMinH"
className="mono"
type="number"
step="1"
min="1"
disabled={open || !!busy || !!plan.semi_armed}
value={semiMinH}
onChange={(e) => {
setSemiMinH(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div>
<div className="field">
<label htmlFor="semiMinLev"></label>
<input
id="semiMinLev"
className="mono"
type="number"
step="1"
min="1"
disabled={open || !!busy || !!plan.semi_armed}
value={semiMinLev}
onChange={(e) => {
setSemiMinLev(Number(e.target.value));
setSemiDirty(true); setSemiDirty(true);
}} }}
/> />
</div> </div>
) : null}
<div className="field">
<label htmlFor="semiPerpU"></label>
<input
id="semiPerpU"
className="mono"
type="number"
step="0.1"
min="0.01"
disabled={open || !!busy || !!plan.semi_armed}
value={semiPerpU}
onChange={(e) => {
setSemiPerpU(Number(e.target.value));
setSemiDirty(true);
}}
/>
</div> </div>
<div className="field"> <div className="mono meta plan-semi-summary">
<label htmlFor="semiOptU"></label> {(() => {
<input const idx = snap?.index_px;
id="semiOptU" if (idx == null || !Number.isFinite(Number(idx))) {
className="mono" return `净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`;
type="number" }
step="0.1" const n = Number(idx);
min="0.01" const tgt =
disabled={open || !!busy || !!plan.semi_armed} semiView === "long"
value={semiOptU} ? n + Number(semiMove)
onChange={(e) => { : n - Number(semiMove);
setSemiOptU(Number(e.target.value)); return `${fmtExPx("index", n)}${fmtExPx("index", tgt)} · 净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U`;
setSemiDirty(true); })()}
}}
/>
</div> </div>
<div className="field"> <div className="plan-semi-btns">
<label htmlFor="semiMove"> · </label> <button
<input className="btn ghost"
id="semiMove" type="button"
className="mono" disabled={open || !!busy || !semiDirty}
type="number" onClick={() => void saveSemiParams()}
step="1" >
min="1"
</button>
<button
className="btn"
type="button"
disabled={open || !!busy || !!plan.semi_armed} disabled={open || !!busy || !!plan.semi_armed}
value={semiMove} onClick={() => void armSemi(true)}
onChange={(e) => { >
setSemiMove(Number(e.target.value));
setSemiDirty(true); </button>
}} <button
/> className="btn ghost"
type="button"
disabled={open || !!busy || !plan.semi_armed}
onClick={() => void armSemi(false)}
>
</button>
</div> </div>
<div className="field"> </section>
<label htmlFor="semiExitU">U×k</label>
<input <section className="card plan-t-quote" aria-label="T型报价">
id="semiExitU" <div className="plan-semi-head">
className="mono" <h3 className="plan-panel-title">T · / / </h3>
type="number" <span className="mono meta">
step="0.1" {ladder?.expiry_ymd
min="0.1" ? `到期 ${ladder.expiry_ymd}`
disabled={open || !!busy || !!plan.semi_armed} : "—"}
value={semiExitU} {ladder?.atm_strike != null
onChange={(e) => { ? ` · ATM ${Math.round(ladder.atm_strike)}`
setSemiExitU(Number(e.target.value)); : ""}
setSemiDirty(true); {ladder?.index_px != null
}} ? ` · 指数 ${fmtExPx("index", ladder.index_px)}`
/> : ""}
</span>
</div> </div>
<div className="field"> {!ladder?.ok ? (
<label htmlFor="semiMinH"> · h</label> <p className="meta">{ladder?.detail || "暂无报价链"}</p>
<input ) : (
id="semiMinH" <div className="plan-t-wrap">
className="mono" <table className="plan-t-table">
type="number" <thead>
step="1" <tr>
min="1" <th>Call卖一</th>
disabled={open || !!busy || !!plan.semi_armed} <th>Call杠杆</th>
value={semiMinH} <th></th>
onChange={(e) => { <th></th>
setSemiMinH(Number(e.target.value)); <th>Put杠杆</th>
setSemiDirty(true); <th>Put卖一</th>
}} </tr>
/> </thead>
</div> <tbody>
<div className="field"> {[...(ladder.rows || [])]
<label htmlFor="semiMinLev"> · </label> .slice()
<input .reverse()
id="semiMinLev" .map((r) => {
className="mono" const tagZh =
type="number" r.tag === "atm"
step="1" ? "平值"
min="1" : r.tag === "otm_call"
disabled={open || !!busy || !!plan.semi_armed} ? "Call虚/Put实"
value={semiMinLev} : "Put虚/Call实";
onChange={(e) => { const hi =
setSemiMinLev(Number(e.target.value)); (semiMny === "otm" &&
setSemiDirty(true); ((semiView === "long" && r.tag === "otm_call") ||
}} (semiView === "short" && r.tag === "otm_put")) &&
/> Math.abs(r.offset) <= Number(semiOtmOff) + 1e-9) ||
</div> (semiMny === "atm" && r.tag === "atm") ||
</div> (semiMny === "itm" &&
<div className="mono meta" style={{ marginTop: 8 }}> ((semiView === "long" &&
{(() => { (r.tag === "otm_put" || r.tag === "atm")) ||
const idx = snap?.index_px; (semiView === "short" &&
if (idx == null || !Number.isFinite(Number(idx))) { (r.tag === "otm_call" || r.tag === "atm"))));
return `目标净利≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · 指数 —`; return (
} <tr
const n = Number(idx); key={r.strike}
const tgt = className={
semiView === "long" ? n + Number(semiMove) : n - Number(semiMove); r.tag === "atm"
const mnyZh = ? "plan-t-atm"
semiMny === "itm" ? "实值" : semiMny === "atm" ? "平值" : "虚值"; : hi
return `指数 ${fmtExPx("index", n)} → 到点 ${fmtExPx("index", tgt)} · ${mnyZh}${ ? "plan-t-pick"
semiMny === "otm" ? `${fmt(semiOtmOff, 0)}` : "" : undefined
} · 配比 ${fmt(semiPerpU, 2)}:${fmt(semiOptU, 2)} · 净利目标≈${fmt(plan.semi_net_exit_target ?? semiExitU, 2)}U · ${plan.semi_armed ? "已授权盯开" : "未授权"}`; }
})()} >
</div> <td className="mono">
<div className="plan-actions" style={{ marginTop: 10, paddingTop: 0 }}> {r.call_ask != null
<button ? fmtExPx("option", r.call_ask)
className="btn ghost" : "—"}
type="button" </td>
disabled={open || !!busy || !semiDirty} <td className="mono">
onClick={() => void saveSemiParams()} {r.call_lev != null
> ? `${r.call_lev.toFixed(0)}x`
: "—"}
</button> </td>
<button <td className="mono plan-t-k">
className="btn" {Math.round(r.strike)}
type="button" <span className="meta">
disabled={open || !!busy || !!plan.semi_armed} {" "}
onClick={() => void armSemi(true)} ({r.offset >= 0 ? "+" : ""}
> {fmt(r.offset, 0)})
</span>
</button> </td>
<button <td>{tagZh}</td>
className="btn ghost" <td className="mono">
type="button" {r.put_lev != null
disabled={open || !!busy || !plan.semi_armed} ? `${r.put_lev.toFixed(0)}x`
onClick={() => void armSemi(false)} : "—"}
> </td>
<td className="mono">
</button> {r.put_ask != null
</div> ? fmtExPx("option", r.put_ask)
</section> : "—"}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</div>
) : null} ) : null}
{!semiOn || planTab === "monitor" ? (
<div className="plan-shell"> <div className="plan-shell">
<div className="plan-board"> <div className="plan-board">
<div className="card plan-strategy"> <div className="card plan-strategy">
@@ -798,7 +973,18 @@ export default function PlanPage() {
<div className="kv"> <div className="kv">
<span></span> <span></span>
<span className="mono"> <span className="mono">
{riskBased ? ( {semiOn ? (
<span className="status-running">
·{" "}
{semiMny === "itm"
? "实值"
: semiMny === "atm"
? "平值"
: "虚值"}{" "}
· {fmt(semiPerpU, 0)}:{fmt(semiOptU, 0)}
{plan?.semi_armed ? " · 已授权" : " · 未授权"}
</span>
) : riskBased ? (
<span className="status-running">{sizingModeLabel}</span> <span className="status-running">{sizingModeLabel}</span>
) : ( ) : (
sizingModeLabel sizingModeLabel
@@ -808,7 +994,17 @@ export default function PlanPage() {
<div className="kv"> <div className="kv">
<span></span> <span></span>
<span className="mono"> <span className="mono">
{isOo ? ( {semiOn ? (
<>
{fmt(semiMinH, 0)}h · {fmt(semiMinLev, 0)}x
{semiMny === "otm"
? ` · 虚值≤${fmt(semiOtmOff, 0)}`
: ""}
{" · "}
±{fmt(semiMove, 0)}&gt;0 /
{fmt(semiExitU, 1)}×k
</>
) : isOo ? (
ooSelectLabel ooSelectLabel
) : ( ) : (
<> <>
@@ -1326,7 +1522,8 @@ export default function PlanPage() {
</div> </div>
<div className="plan-market"> <div className="plan-market">
<div className="card"> {showAmpCard ? (
<div className="card plan-card-compact">
<h3 className="plan-panel-title"> / </h3> <h3 className="plan-panel-title"> / </h3>
<div className="kv"> <div className="kv">
<span></span> <span></span>
@@ -1375,6 +1572,7 @@ export default function PlanPage() {
</span> </span>
</div> </div>
</div> </div>
) : null}
{!isOo ? ( {!isOo ? (
<div className="card"> <div className="card">
<h3 className="plan-panel-title"></h3> <h3 className="plan-panel-title"></h3>
@@ -1617,6 +1815,7 @@ export default function PlanPage() {
</div> </div>
) : null} ) : null}
</div> </div>
) : null}
</div> </div>
); );
} }
+138
View File
@@ -468,6 +468,144 @@ input {
color: #5ec8ff; color: #5ec8ff;
} }
.plan-main-tabs {
margin-top: 4px;
margin-bottom: 10px;
}
.plan-semi-tab {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.plan-semi-compact {
padding: 10px 12px;
}
.plan-semi-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.plan-semi-head .plan-panel-title {
margin: 0;
font-size: 13px;
}
.plan-semi-rules {
margin: 0 0 8px;
font-size: 12px;
color: var(--muted);
}
.plan-semi-rules summary {
cursor: pointer;
color: #5ec8ff;
user-select: none;
}
.plan-semi-rules ul {
margin: 6px 0 0;
padding-left: 1.2em;
line-height: 1.45;
}
.plan-semi-fields {
margin-top: 0 !important;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important;
gap: 8px !important;
}
.plan-semi-fields .field label {
font-size: 11px;
}
.plan-semi-fields .field input,
.plan-semi-fields .field select {
padding: 6px 8px;
font-size: 12px;
min-height: 32px;
}
.plan-semi-summary {
margin-top: 6px !important;
font-size: 12px;
}
.plan-semi-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.plan-semi-btns .btn {
min-height: 34px;
padding: 6px 12px;
font-size: 13px;
}
.plan-t-quote {
padding: 10px 12px;
}
.plan-t-wrap {
overflow-x: auto;
margin-top: 4px;
}
.plan-t-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.plan-t-table th,
.plan-t-table td {
padding: 4px 6px;
border-bottom: 1px solid var(--line);
text-align: right;
white-space: nowrap;
}
.plan-t-table th:nth-child(3),
.plan-t-table td:nth-child(3),
.plan-t-table th:nth-child(4),
.plan-t-table td:nth-child(4) {
text-align: center;
}
.plan-t-table th {
color: var(--muted);
font-weight: 600;
}
.plan-t-atm {
background: rgba(94, 200, 255, 0.08);
}
.plan-t-pick {
background: rgba(14, 203, 129, 0.08);
}
.plan-t-k {
font-weight: 700;
}
.plan-card-compact {
padding: 10px 12px;
}
.plan-card-compact .plan-panel-title {
margin-bottom: 6px;
font-size: 13px;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.app-container { .app-container {
padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px)); padding: 0 12px calc(72px + env(safe-area-inset-bottom, 0px));