Show option expiry countdown and widen settings for ultrawide/tablet.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 09:24:11 +08:00
parent 37894e6f0f
commit 77ae6a44b9
5 changed files with 406 additions and 190 deletions
+42
View File
@@ -11,6 +11,28 @@ function pnlClass(n: number | null | undefined) {
return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss";
}
/** OKX 期权到期:YYMMDD 当日 08:00 UTC(上海 16:00 */
function expiryMsFromYmd(ymd: string | null | undefined): number | null {
if (!ymd || ymd.length !== 6) return null;
const yy = 2000 + Number(ymd.slice(0, 2));
const mm = Number(ymd.slice(2, 4));
const dd = Number(ymd.slice(4, 6));
if (![yy, mm, dd].every((x) => Number.isFinite(x))) return null;
return Date.UTC(yy, mm - 1, dd, 8, 0, 0);
}
function formatCountdown(msLeft: number): string {
if (msLeft <= 0) return "已到期";
const s = Math.floor(msLeft / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (d > 0) return `${d}${h}${String(m).padStart(2, "0")}`;
if (h > 0) return `${h}${String(m).padStart(2, "0")}${String(sec).padStart(2, "0")}`;
return `${m}${String(sec).padStart(2, "0")}`;
}
const PHASE_ZH: Record<string, string> = {
idle: "空闲",
wait_signal: "等待信号",
@@ -29,6 +51,7 @@ export default function PlanPage() {
const [plan, setPlan] = useState<PlanState | null>(null);
const [err, setErr] = useState("");
const [busy, setBusy] = useState("");
const [nowMs, setNowMs] = useState(() => Date.now());
async function refresh() {
try {
@@ -50,6 +73,11 @@ export default function PlanPage() {
return () => window.clearInterval(t);
}, []);
useEffect(() => {
const t = window.setInterval(() => setNowMs(Date.now()), 1000);
return () => window.clearInterval(t);
}, []);
async function act(path: string, label: string) {
setBusy(label);
setErr("");
@@ -84,6 +112,12 @@ export default function PlanPage() {
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
const expiryMs =
pos?.expiry_ms ?? expiryMsFromYmd(pos?.expiry_ymd) ?? null;
const countdown =
open && expiryMs != null ? formatCountdown(expiryMs - nowMs) : "—";
const countdownUrgent =
open && expiryMs != null && expiryMs - nowMs > 0 && expiryMs - nowMs < 6 * 3600_000;
return (
<div>
@@ -290,6 +324,14 @@ export default function PlanPage() {
</span>
</div>
<div className="pos-grid">
<div className="pos-cell pos-cell-wide">
<span className="pos-label"></span>
<span
className={`pos-value mono ${countdownUrgent ? "pos-countdown-urgent" : ""}`}
>
{countdown}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">{fmt(pos?.option_entry_px)}</span>
+209 -188
View File
@@ -112,7 +112,7 @@ export default function SettingsPage() {
}
return (
<div style={{ maxWidth: 560 }}>
<div className="settings-page">
<h2 style={{ marginTop: 0 }}></h2>
<div className="tabs">
<button
@@ -132,207 +132,228 @@ export default function SettingsPage() {
</div>
{tab === "strategy" ? (
<div className="card">
<p style={{ color: "var(--muted)", marginTop: 0 }}>
<div className="card settings-card">
<p className="settings-lead">
</p>
{stratOk ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{stratOk}</div> : null}
{stratOk ? <div className="settings-ok">{stratOk}</div> : null}
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
<form onSubmit={onSaveStrategy}>
<h3 style={{ margin: "0 0 10px", fontSize: 14, color: "var(--muted)" }}></h3>
<div className="field">
<label htmlFor="lev"></label>
<input
id="lev"
className="mono"
type="number"
step="1"
min="1"
value={leverage}
onChange={(e) => setLeverage(Number(e.target.value))}
/>
</div>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="lev"></label>
<input
id="lev"
className="mono"
type="number"
step="1"
min="1"
value={leverage}
onChange={(e) => setLeverage(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="perp"> ETH </label>
<input
id="perp"
className="mono"
type="number"
step="0.01"
min="0.01"
value={perpQty}
onChange={(e) => setPerpQty(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="opt"> ETH </label>
<input
id="opt"
className="mono"
type="number"
step="0.01"
min="0.01"
value={optQty}
onChange={(e) => setOptQty(Number(e.target.value))}
/>
</div>
</div>
</section>
<h3 style={{ margin: "18px 0 10px", fontSize: 14, color: "var(--muted)" }}></h3>
<div className="field">
<label htmlFor="hours"></label>
<input
id="hours"
className="mono"
type="number"
step="1"
min="1"
value={minHours}
onChange={(e) => setMinHours(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="olev">÷</label>
<input
id="olev"
className="mono"
type="number"
step="1"
min="1"
value={minOptLev}
onChange={(e) => setMinOptLev(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="exitMode"></label>
<select
id="exitMode"
className="mono"
value={exitMode}
onChange={(e) =>
setExitMode(
e.target.value === "premium_multiple"
? "premium_multiple"
: "fixed_usdt",
)
}
>
<option value="fixed_usdt">USDT</option>
<option value="premium_multiple"></option>
</select>
</div>
{exitMode === "fixed_usdt" ? (
<div className="field">
<label htmlFor="netTarget">USDT</label>
<input
id="netTarget"
className="mono"
type="number"
step="0.1"
min="0.1"
value={netTarget}
onChange={(e) => setNetTarget(Number(e.target.value))}
/>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="hours"></label>
<input
id="hours"
className="mono"
type="number"
step="1"
min="1"
value={minHours}
onChange={(e) => setMinHours(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="olev">÷</label>
<input
id="olev"
className="mono"
type="number"
step="1"
min="1"
value={minOptLev}
onChange={(e) => setMinOptLev(Number(e.target.value))}
/>
</div>
</div>
) : (
<div className="field">
<label htmlFor="premMult">1 = </label>
<input
id="premMult"
className="mono"
type="number"
step="0.1"
min="0.1"
value={premMult}
onChange={(e) => setPremMult(Number(e.target.value))}
/>
</section>
<section className="settings-section">
<h3></h3>
<div className="settings-fields">
<div className="field">
<label htmlFor="exitMode"></label>
<select
id="exitMode"
className="mono"
value={exitMode}
onChange={(e) =>
setExitMode(
e.target.value === "premium_multiple"
? "premium_multiple"
: "fixed_usdt",
)
}
>
<option value="fixed_usdt">USDT</option>
<option value="premium_multiple"></option>
</select>
</div>
{exitMode === "fixed_usdt" ? (
<div className="field">
<label htmlFor="netTarget">USDT</label>
<input
id="netTarget"
className="mono"
type="number"
step="0.1"
min="0.1"
value={netTarget}
onChange={(e) => setNetTarget(Number(e.target.value))}
/>
</div>
) : (
<div className="field">
<label htmlFor="premMult">1 = </label>
<input
id="premMult"
className="mono"
type="number"
step="0.1"
min="0.1"
value={premMult}
onChange={(e) => setPremMult(Number(e.target.value))}
/>
</div>
)}
<div className="field">
<label htmlFor="closeDev">/%</label>
<input
id="closeDev"
className="mono"
type="number"
step="1"
min="1"
value={closeDevPct}
onChange={(e) => setCloseDevPct(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="rest"></label>
<input
id="rest"
className="mono"
type="number"
step="1"
value={rest}
onChange={(e) => setRest(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="fee"></label>
<input
id="fee"
className="mono"
type="number"
step="0.0001"
value={fee}
onChange={(e) => setFee(Number(e.target.value))}
/>
</div>
</div>
)}
<div className="field">
<label htmlFor="closeDev">/%</label>
<input
id="closeDev"
className="mono"
type="number"
step="1"
min="1"
value={closeDevPct}
onChange={(e) => setCloseDevPct(Number(e.target.value))}
/>
</section>
<div className="settings-actions">
<button className="btn" type="submit">
</button>
</div>
<div className="field">
<label htmlFor="perp"> ETH </label>
<input
id="perp"
className="mono"
type="number"
step="0.01"
min="0.01"
value={perpQty}
onChange={(e) => setPerpQty(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="opt"> ETH </label>
<input
id="opt"
className="mono"
type="number"
step="0.01"
min="0.01"
value={optQty}
onChange={(e) => setOptQty(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="rest"></label>
<input
id="rest"
className="mono"
type="number"
step="1"
value={rest}
onChange={(e) => setRest(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="fee"></label>
<input
id="fee"
className="mono"
type="number"
step="0.0001"
value={fee}
onChange={(e) => setFee(Number(e.target.value))}
/>
</div>
<button className="btn" type="submit">
</button>
</form>
</div>
) : (
<div className="card">
<div className="card settings-card settings-card-narrow">
{err ? <div className="err">{err}</div> : null}
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
<form onSubmit={onSaveCreds}>
<div className="field">
<label htmlFor="user"></label>
<input
id="user"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
required
/>
{ok ? <div className="settings-ok">{ok}</div> : null}
<form onSubmit={onSaveCreds} className="settings-account-form">
<div className="settings-fields">
<div className="field">
<label htmlFor="user"></label>
<input
id="user"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="cur"></label>
<input
id="cur"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="np"></label>
<input
id="np"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="cp"></label>
<input
id="cp"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
</div>
<div className="field">
<label htmlFor="cur"></label>
<input
id="cur"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
<div className="settings-actions">
<button className="btn" type="submit" disabled={loading}>
{loading ? "保存中…" : "保存账号"}
</button>
</div>
<div className="field">
<label htmlFor="np"></label>
<input
id="np"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</div>
<div className="field">
<label htmlFor="cp"></label>
<input
id="cp"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
<button className="btn" type="submit" disabled={loading}>
{loading ? "保存中…" : "保存账号"}
</button>
</form>
</div>
)}