Show option expiry countdown and widen settings for ultrawide/tablet.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -507,6 +507,14 @@ class Matcher:
|
|||||||
)
|
)
|
||||||
strike = float(g["strike"]) if g and g["strike"] is not None else None
|
strike = float(g["strike"]) if g and g["strike"] is not None else None
|
||||||
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
|
expiry_ymd = str(g["expiry_ymd"]) if g and g["expiry_ymd"] else None
|
||||||
|
expiry_ms = None
|
||||||
|
if expiry_ymd and len(expiry_ymd) == 6:
|
||||||
|
try:
|
||||||
|
from ..exchange.okx.parse import expiry_ms_from_ymd
|
||||||
|
|
||||||
|
expiry_ms = expiry_ms_from_ymd(expiry_ymd)
|
||||||
|
except Exception:
|
||||||
|
expiry_ms = None
|
||||||
perp_inst_id = (
|
perp_inst_id = (
|
||||||
str(g["perp_inst_id"])
|
str(g["perp_inst_id"])
|
||||||
if g and g["perp_inst_id"]
|
if g and g["perp_inst_id"]
|
||||||
@@ -532,6 +540,7 @@ class Matcher:
|
|||||||
"option_mark_px": float(opt_mark) if opt_mark is not None else None,
|
"option_mark_px": float(opt_mark) if opt_mark is not None else None,
|
||||||
"strike": strike,
|
"strike": strike,
|
||||||
"expiry_ymd": expiry_ymd,
|
"expiry_ymd": expiry_ymd,
|
||||||
|
"expiry_ms": expiry_ms,
|
||||||
"perp_upl": perp_upl,
|
"perp_upl": perp_upl,
|
||||||
"option_upl": option_upl,
|
"option_upl": option_upl,
|
||||||
"est_close_fees": est_close_fees,
|
"est_close_fees": est_close_fees,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export type PlanState = {
|
|||||||
option_mark_px?: number | null;
|
option_mark_px?: number | null;
|
||||||
strike?: number | null;
|
strike?: number | null;
|
||||||
expiry_ymd?: string | null;
|
expiry_ymd?: string | null;
|
||||||
|
expiry_ms?: number | null;
|
||||||
perp_upl?: number;
|
perp_upl?: number;
|
||||||
option_upl?: number;
|
option_upl?: number;
|
||||||
est_close_fees?: number;
|
est_close_fees?: number;
|
||||||
|
|||||||
@@ -11,6 +11,28 @@ function pnlClass(n: number | null | undefined) {
|
|||||||
return n > 0 ? "pos-pnl-profit" : "pos-pnl-loss";
|
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> = {
|
const PHASE_ZH: Record<string, string> = {
|
||||||
idle: "空闲",
|
idle: "空闲",
|
||||||
wait_signal: "等待信号",
|
wait_signal: "等待信号",
|
||||||
@@ -29,6 +51,7 @@ export default function PlanPage() {
|
|||||||
const [plan, setPlan] = useState<PlanState | null>(null);
|
const [plan, setPlan] = useState<PlanState | null>(null);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [busy, setBusy] = useState("");
|
const [busy, setBusy] = useState("");
|
||||||
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +73,11 @@ export default function PlanPage() {
|
|||||||
return () => window.clearInterval(t);
|
return () => window.clearInterval(t);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const t = window.setInterval(() => setNowMs(Date.now()), 1000);
|
||||||
|
return () => window.clearInterval(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function act(path: string, label: string) {
|
async function act(path: string, label: string) {
|
||||||
setBusy(label);
|
setBusy(label);
|
||||||
setErr("");
|
setErr("");
|
||||||
@@ -84,6 +112,12 @@ export default function PlanPage() {
|
|||||||
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
|
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
|
||||||
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
||||||
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -290,6 +324,14 @@ export default function PlanPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pos-grid">
|
<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">
|
<div className="pos-cell">
|
||||||
<span className="pos-label">开仓均价</span>
|
<span className="pos-label">开仓均价</span>
|
||||||
<span className="pos-value mono">{fmt(pos?.option_entry_px)}</span>
|
<span className="pos-value mono">{fmt(pos?.option_entry_px)}</span>
|
||||||
|
|||||||
+209
-188
@@ -112,7 +112,7 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 560 }}>
|
<div className="settings-page">
|
||||||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
<button
|
<button
|
||||||
@@ -132,207 +132,228 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === "strategy" ? (
|
{tab === "strategy" ? (
|
||||||
<div className="card">
|
<div className="card settings-card">
|
||||||
<p style={{ color: "var(--muted)", marginTop: 0 }}>
|
<p className="settings-lead">
|
||||||
无开仓时间窗;期权按剩余时长选到期 → 平值 → 校验杠杆;出场可选固定金额或权利金倍数。
|
无开仓时间窗;期权按剩余时长选到期 → 平值 → 校验杠杆;出场可选固定金额或权利金倍数。
|
||||||
</p>
|
</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}
|
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
|
||||||
<form onSubmit={onSaveStrategy}>
|
<form onSubmit={onSaveStrategy}>
|
||||||
<h3 style={{ margin: "0 0 10px", fontSize: 14, color: "var(--muted)" }}>资金</h3>
|
<section className="settings-section">
|
||||||
<div className="field">
|
<h3>资金</h3>
|
||||||
<label htmlFor="lev">永续杠杆倍数</label>
|
<div className="settings-fields">
|
||||||
<input
|
<div className="field">
|
||||||
id="lev"
|
<label htmlFor="lev">永续杠杆倍数</label>
|
||||||
className="mono"
|
<input
|
||||||
type="number"
|
id="lev"
|
||||||
step="1"
|
className="mono"
|
||||||
min="1"
|
type="number"
|
||||||
value={leverage}
|
step="1"
|
||||||
onChange={(e) => setLeverage(Number(e.target.value))}
|
min="1"
|
||||||
/>
|
value={leverage}
|
||||||
</div>
|
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>
|
<section className="settings-section">
|
||||||
<div className="field">
|
<h3>选约</h3>
|
||||||
<label htmlFor="hours">期权最小剩余小时</label>
|
<div className="settings-fields">
|
||||||
<input
|
<div className="field">
|
||||||
id="hours"
|
<label htmlFor="hours">期权最小剩余小时</label>
|
||||||
className="mono"
|
<input
|
||||||
type="number"
|
id="hours"
|
||||||
step="1"
|
className="mono"
|
||||||
min="1"
|
type="number"
|
||||||
value={minHours}
|
step="1"
|
||||||
onChange={(e) => setMinHours(Number(e.target.value))}
|
min="1"
|
||||||
/>
|
value={minHours}
|
||||||
</div>
|
onChange={(e) => setMinHours(Number(e.target.value))}
|
||||||
<div className="field">
|
/>
|
||||||
<label htmlFor="olev">期权杠杆下限(现价÷卖一权利金)</label>
|
</div>
|
||||||
<input
|
<div className="field">
|
||||||
id="olev"
|
<label htmlFor="olev">期权杠杆下限(现价÷卖一权利金)</label>
|
||||||
className="mono"
|
<input
|
||||||
type="number"
|
id="olev"
|
||||||
step="1"
|
className="mono"
|
||||||
min="1"
|
type="number"
|
||||||
value={minOptLev}
|
step="1"
|
||||||
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
min="1"
|
||||||
/>
|
value={minOptLev}
|
||||||
</div>
|
onChange={(e) => setMinOptLev(Number(e.target.value))}
|
||||||
<div className="field">
|
/>
|
||||||
<label htmlFor="exitMode">出场规则</label>
|
</div>
|
||||||
<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>
|
||||||
) : (
|
</section>
|
||||||
<div className="field">
|
|
||||||
<label htmlFor="premMult">权利金倍数(1 = 一倍权利金)</label>
|
<section className="settings-section">
|
||||||
<input
|
<h3>出场与风控</h3>
|
||||||
id="premMult"
|
<div className="settings-fields">
|
||||||
className="mono"
|
<div className="field">
|
||||||
type="number"
|
<label htmlFor="exitMode">出场规则</label>
|
||||||
step="0.1"
|
<select
|
||||||
min="0.1"
|
id="exitMode"
|
||||||
value={premMult}
|
className="mono"
|
||||||
onChange={(e) => setPremMult(Number(e.target.value))}
|
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>
|
||||||
)}
|
</section>
|
||||||
<div className="field">
|
|
||||||
<label htmlFor="closeDev">平仓买一/标记最大偏差(%)</label>
|
<div className="settings-actions">
|
||||||
<input
|
<button className="btn" type="submit">
|
||||||
id="closeDev"
|
保存策略
|
||||||
className="mono"
|
</button>
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
min="1"
|
|
||||||
value={closeDevPct}
|
|
||||||
onChange={(e) => setCloseDevPct(Number(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="card">
|
<div className="card settings-card settings-card-narrow">
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
{ok ? <div style={{ color: "var(--up)", marginBottom: 12 }}>{ok}</div> : null}
|
{ok ? <div className="settings-ok">{ok}</div> : null}
|
||||||
<form onSubmit={onSaveCreds}>
|
<form onSubmit={onSaveCreds} className="settings-account-form">
|
||||||
<div className="field">
|
<div className="settings-fields">
|
||||||
<label htmlFor="user">新用户名</label>
|
<div className="field">
|
||||||
<input
|
<label htmlFor="user">新用户名</label>
|
||||||
id="user"
|
<input
|
||||||
value={newUsername}
|
id="user"
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
value={newUsername}
|
||||||
required
|
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>
|
||||||
<div className="field">
|
<div className="settings-actions">
|
||||||
<label htmlFor="cur">当前密码</label>
|
<button className="btn" type="submit" disabled={loading}>
|
||||||
<input
|
{loading ? "保存中…" : "保存账号"}
|
||||||
id="cur"
|
</button>
|
||||||
type="password"
|
|
||||||
value={currentPassword}
|
|
||||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+145
-2
@@ -145,12 +145,49 @@ input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
padding: 16px;
|
padding: 16px 20px 28px;
|
||||||
max-width: 1200px;
|
max-width: min(1680px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1800px) {
|
||||||
|
.page {
|
||||||
|
max-width: 1760px;
|
||||||
|
padding-left: 28px;
|
||||||
|
padding-right: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.page {
|
||||||
|
padding: 12px 14px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-side.left,
|
||||||
|
.topnav-side.right {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav-center {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topnav a {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -233,6 +270,8 @@ input {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field input:focus,
|
.field input:focus,
|
||||||
@@ -240,6 +279,96 @@ input {
|
|||||||
border-color: rgba(240, 185, 11, 0.55);
|
border-color: rgba(240, 185, 11, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings: ultrawide + tablet */
|
||||||
|
.settings-page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-narrow {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-lead {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-ok {
|
||||||
|
color: var(--up);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section:last-of-type {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-fields .field {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 700px) {
|
||||||
|
.settings-fields {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1100px) {
|
||||||
|
.settings-page {
|
||||||
|
max-width: 1320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-fields {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 699px) {
|
||||||
|
.settings-fields .field {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field select {
|
||||||
|
min-height: 44px;
|
||||||
|
font-size: 16px; /* 避免 iOS 自动缩放 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -385,12 +514,26 @@ input {
|
|||||||
gap: 12px 14px;
|
gap: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pos-cell-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-countdown-urgent {
|
||||||
|
color: #ffb020 !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.pos-grid {
|
.pos-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.pos-embed-grid {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pos-cell {
|
.pos-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user