dbc86a1ce6
OKX/Binance LIVE share half_open and option_closed_perp_pending repair paths; private REST throttles default to 1s and are tunable in settings. Co-authored-by: Cursor <cursoragent@cursor.com>
704 lines
26 KiB
TypeScript
704 lines
26 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
||
import {
|
||
changeCredentials,
|
||
getUsername,
|
||
setSession,
|
||
apiFetch,
|
||
StrategySettings,
|
||
RuntimeSettings,
|
||
} from "../api/client";
|
||
|
||
type Tab = "strategy" | "runtime" | "account";
|
||
|
||
export default function SettingsPage() {
|
||
const [tab, setTab] = useState<Tab>("strategy");
|
||
|
||
const [newUsername, setNewUsername] = useState(getUsername() || "admin");
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [err, setErr] = useState("");
|
||
const [ok, setOk] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const [fee, setFee] = useState(0.0005);
|
||
const [exitMode, setExitMode] = useState<"fixed_usdt" | "premium_multiple">(
|
||
"fixed_usdt",
|
||
);
|
||
const [netTarget, setNetTarget] = useState(15);
|
||
const [premMult, setPremMult] = useState(1);
|
||
const [rest, setRest] = useState(300);
|
||
const [orderInterval, setOrderInterval] = useState(1);
|
||
const [skipWeekends, setSkipWeekends] = useState(true);
|
||
const [leverage, setLeverage] = useState(3);
|
||
const [minHours, setMinHours] = useState(12);
|
||
const [minOptLev, setMinOptLev] = useState(100);
|
||
const [atmOffOn, setAtmOffOn] = useState(false);
|
||
const [maxAtmOff, setMaxAtmOff] = useState(3);
|
||
const [closeDevPct, setCloseDevPct] = useState(30);
|
||
const [perpQty, setPerpQty] = useState(1);
|
||
const [optQty, setOptQty] = useState(2);
|
||
const [initialEquity, setInitialEquity] = useState(10000);
|
||
const [exchange, setExchange] = useState<"okx" | "binance">("okx");
|
||
const [stratOk, setStratOk] = useState("");
|
||
|
||
const [runtime, setRuntime] = useState<RuntimeSettings | null>(null);
|
||
const [mode, setMode] = useState<"SIM" | "LIVE">("SIM");
|
||
const [okxKey, setOkxKey] = useState("");
|
||
const [okxSecret, setOkxSecret] = useState("");
|
||
const [okxPass, setOkxPass] = useState("");
|
||
const [bnKey, setBnKey] = useState("");
|
||
const [bnSecret, setBnSecret] = useState("");
|
||
const [runtimeOk, setRuntimeOk] = useState("");
|
||
|
||
function loadRuntime() {
|
||
apiFetch<RuntimeSettings>("/api/settings/runtime")
|
||
.then((r) => {
|
||
setRuntime(r);
|
||
setMode(r.mode === "LIVE" ? "LIVE" : "SIM");
|
||
})
|
||
.catch(() => undefined);
|
||
}
|
||
|
||
useEffect(() => {
|
||
apiFetch<StrategySettings>("/api/settings/strategy")
|
||
.then((s) => {
|
||
setFee(s.fee_rate);
|
||
setExitMode(s.exit_mode === "premium_multiple" ? "premium_multiple" : "fixed_usdt");
|
||
setNetTarget(s.net_profit_target ?? 15);
|
||
setPremMult(s.premium_exit_multiple ?? 1);
|
||
setRest(s.rest_seconds);
|
||
setOrderInterval(s.live_order_interval_sec ?? 1);
|
||
setSkipWeekends(s.skip_weekends !== false);
|
||
setLeverage(s.leverage ?? 3);
|
||
setMinHours(s.min_option_hours ?? 12);
|
||
setMinOptLev(s.min_option_leverage ?? 100);
|
||
setAtmOffOn(s.atm_open_offset_enabled === true);
|
||
setMaxAtmOff(s.max_atm_open_offset ?? 3);
|
||
setCloseDevPct(s.close_bid_mark_max_pct ?? 30);
|
||
setPerpQty(s.perp_qty_eth ?? 1);
|
||
setOptQty(s.option_qty_eth ?? 2);
|
||
setInitialEquity(s.initial_equity ?? 10000);
|
||
setExchange(s.exchange === "binance" ? "binance" : "okx");
|
||
})
|
||
.catch(() => undefined);
|
||
loadRuntime();
|
||
}, []);
|
||
|
||
async function onSaveCreds(e: FormEvent) {
|
||
e.preventDefault();
|
||
setErr("");
|
||
setOk("");
|
||
if (newPassword !== confirmPassword) {
|
||
setErr("两次输入的新密码不一致");
|
||
return;
|
||
}
|
||
if (newPassword.length < 4) {
|
||
setErr("新密码至少 4 位");
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
const res = await changeCredentials({
|
||
current_password: currentPassword,
|
||
new_username: newUsername.trim(),
|
||
new_password: newPassword,
|
||
});
|
||
setSession(res.token, res.username);
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
setOk("用户名/密码已更新");
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function onSaveStrategy(e: FormEvent) {
|
||
e.preventDefault();
|
||
setStratOk("");
|
||
setErr("");
|
||
try {
|
||
await apiFetch("/api/settings/strategy", {
|
||
method: "PUT",
|
||
body: JSON.stringify({
|
||
fee_rate: fee,
|
||
exit_mode: exitMode,
|
||
net_profit_target: netTarget,
|
||
premium_exit_multiple: premMult,
|
||
rest_seconds: rest,
|
||
live_order_interval_sec: orderInterval,
|
||
skip_weekends: skipWeekends,
|
||
leverage,
|
||
min_option_hours: minHours,
|
||
min_option_leverage: minOptLev,
|
||
atm_open_offset_enabled: atmOffOn,
|
||
max_atm_open_offset: maxAtmOff,
|
||
close_bid_mark_max_pct: closeDevPct,
|
||
perp_qty_eth: perpQty,
|
||
option_qty_eth: optQty,
|
||
initial_equity: initialEquity,
|
||
exchange,
|
||
}),
|
||
});
|
||
setStratOk(
|
||
"策略参数已保存(切换交易所/改模拟资金需无持仓;切换后会重连行情)",
|
||
);
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
}
|
||
}
|
||
|
||
async function onSaveRuntime(e: FormEvent) {
|
||
e.preventDefault();
|
||
setErr("");
|
||
setRuntimeOk("");
|
||
const goingLive = mode === "LIVE" && runtime?.mode !== "LIVE";
|
||
if (goingLive) {
|
||
const typed = window.prompt('切换到 LIVE 实盘:请输入 LIVE 确认(将真实下单)');
|
||
if (typed !== "LIVE") {
|
||
setErr("已取消:须输入 LIVE 才能切换到实盘");
|
||
return;
|
||
}
|
||
}
|
||
setLoading(true);
|
||
try {
|
||
const body: Record<string, unknown> = {
|
||
mode,
|
||
confirm_live: goingLive,
|
||
};
|
||
if (okxKey.trim()) body.okx_api_key = okxKey.trim();
|
||
if (okxSecret.trim()) body.okx_api_secret = okxSecret.trim();
|
||
if (okxPass.trim()) body.okx_api_passphrase = okxPass.trim();
|
||
if (bnKey.trim()) body.binance_api_key = bnKey.trim();
|
||
if (bnSecret.trim()) body.binance_api_secret = bnSecret.trim();
|
||
const r = await apiFetch<RuntimeSettings>("/api/settings/runtime", {
|
||
method: "PUT",
|
||
body: JSON.stringify(body),
|
||
});
|
||
setRuntime(r);
|
||
setMode(r.mode === "LIVE" ? "LIVE" : "SIM");
|
||
setOkxKey("");
|
||
setOkxSecret("");
|
||
setOkxPass("");
|
||
setBnKey("");
|
||
setBnSecret("");
|
||
setRuntimeOk(
|
||
r.mode === "LIVE"
|
||
? r.live_ready
|
||
? "已切换 LIVE,密钥已写入 .env"
|
||
: `已切 LIVE,但未就绪:${r.live_ready_reason}`
|
||
: "已切换 SIM,配置已写入 .env",
|
||
);
|
||
} catch (ex) {
|
||
setErr(ex instanceof Error ? ex.message : String(ex));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="settings-page">
|
||
<h2 style={{ marginTop: 0 }}>系统设置</h2>
|
||
<div className="tabs">
|
||
<button
|
||
type="button"
|
||
className={tab === "strategy" ? "tab active" : "tab"}
|
||
onClick={() => setTab("strategy")}
|
||
>
|
||
策略设置
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={tab === "runtime" ? "tab active" : "tab"}
|
||
onClick={() => {
|
||
setTab("runtime");
|
||
loadRuntime();
|
||
}}
|
||
>
|
||
运行模式
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={tab === "account" ? "tab active" : "tab"}
|
||
onClick={() => setTab("account")}
|
||
>
|
||
登录账户
|
||
</button>
|
||
</div>
|
||
|
||
{tab === "strategy" ? (
|
||
<div className="card settings-card">
|
||
<p className="settings-lead">
|
||
无开仓时间窗;可切换 OKX / 币安行情(SIM 本地撮合);周六日可跳过开仓;出场按净盈利或到期全平。
|
||
</p>
|
||
{stratOk ? <div className="settings-ok">{stratOk}</div> : null}
|
||
{err && tab === "strategy" ? <div className="err">{err}</div> : null}
|
||
<form onSubmit={onSaveStrategy}>
|
||
<section className="settings-section">
|
||
<h3>交易所</h3>
|
||
<div className="settings-fields">
|
||
<div className="field">
|
||
<label htmlFor="exch">行情交易所(SIM 只读)</label>
|
||
<select
|
||
id="exch"
|
||
className="mono"
|
||
value={exchange}
|
||
onChange={(e) =>
|
||
setExchange(e.target.value === "binance" ? "binance" : "okx")
|
||
}
|
||
>
|
||
<option value="okx">OKX(USDC 期权族)</option>
|
||
<option value="binance">币安(USDT 永续 + 欧洲期权)</option>
|
||
</select>
|
||
<p className="settings-hint">
|
||
有持仓时不可切换。切换后自动套用该所合约并重连公共行情。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="settings-section">
|
||
<h3>资金</h3>
|
||
<div className="settings-fields">
|
||
<div className="field">
|
||
<label htmlFor="equity">模拟资金(USDT)</label>
|
||
<input
|
||
id="equity"
|
||
className="mono"
|
||
type="number"
|
||
step="100"
|
||
min="1000"
|
||
value={initialEquity}
|
||
onChange={(e) => setInitialEquity(Number(e.target.value))}
|
||
/>
|
||
<p className="settings-hint">
|
||
保存后将权益重置为该金额(须无持仓)。默认 10000。
|
||
</p>
|
||
</div>
|
||
<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>
|
||
|
||
<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 className="field">
|
||
<label htmlFor="atmoffOn">开仓 ATM 偏差限制</label>
|
||
<select
|
||
id="atmoffOn"
|
||
className="mono"
|
||
value={atmOffOn ? "1" : "0"}
|
||
onChange={(e) => setAtmOffOn(e.target.value === "1")}
|
||
>
|
||
<option value="0">关闭(默认)</option>
|
||
<option value="1">开启</option>
|
||
</select>
|
||
<p className="settings-hint">
|
||
关闭时不限制 |ATM − 现价|;开启后超过下方点数则不开仓。
|
||
</p>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="atmoff">开仓 ATM 最大偏差(点)</label>
|
||
<input
|
||
id="atmoff"
|
||
className="mono"
|
||
type="number"
|
||
step="0.5"
|
||
min="0"
|
||
disabled={!atmOffOn}
|
||
value={maxAtmOff}
|
||
onChange={(e) => setMaxAtmOff(Number(e.target.value))}
|
||
/>
|
||
<p className="settings-hint">
|
||
仅在上方开关开启时生效。默认 3;币安行权价步进较粗时可能长时间无开仓机会。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</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="orderInterval">实盘下单最小间隔(秒)</label>
|
||
<input
|
||
id="orderInterval"
|
||
className="mono"
|
||
type="number"
|
||
step="0.1"
|
||
min="0.2"
|
||
max="30"
|
||
value={orderInterval}
|
||
onChange={(e) => setOrderInterval(Number(e.target.value))}
|
||
/>
|
||
<p className="settings-hint">
|
||
LIVE 私有下单/查单间隔,默认 1s(非高频建议 ≥1)。范围
|
||
0.2–30。保存后立即生效。
|
||
</p>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="skipWe">周六日跳过开仓(上海时区)</label>
|
||
<select
|
||
id="skipWe"
|
||
className="mono"
|
||
value={skipWeekends ? "1" : "0"}
|
||
onChange={(e) => setSkipWeekends(e.target.value === "1")}
|
||
>
|
||
<option value="1">开启(推荐)</option>
|
||
<option value="0">关闭</option>
|
||
</select>
|
||
</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>
|
||
</section>
|
||
|
||
<div className="settings-actions">
|
||
<button className="btn" type="submit">
|
||
保存策略
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
) : null}
|
||
|
||
{tab === "runtime" ? (
|
||
<div className="card settings-card">
|
||
<p className="settings-lead">
|
||
SIM = 本地撮合;LIVE = 当前所选交易所真下单(OKX / 币安均已接入)。密钥只写入服务器{" "}
|
||
<span className="mono">.env</span>,接口不回传明文。OKX 期权常需账户有 USDC;币安多为 USDT。
|
||
</p>
|
||
{runtimeOk ? <div className="settings-ok">{runtimeOk}</div> : null}
|
||
{err && tab === "runtime" ? <div className="err">{err}</div> : null}
|
||
<form onSubmit={onSaveRuntime}>
|
||
<section className="settings-section">
|
||
<h3>模式</h3>
|
||
<div className="settings-fields">
|
||
<div className="field">
|
||
<label htmlFor="mode">运行模式</label>
|
||
<select
|
||
id="mode"
|
||
className="mono"
|
||
value={mode}
|
||
onChange={(e) =>
|
||
setMode(e.target.value === "LIVE" ? "LIVE" : "SIM")
|
||
}
|
||
>
|
||
<option value="SIM">SIM 本地模拟</option>
|
||
<option value="LIVE">LIVE 实盘</option>
|
||
</select>
|
||
<p className="settings-hint">
|
||
当前:{runtime?.mode || "—"} · 交易所 {runtime?.exchange || "—"} ·{" "}
|
||
{runtime?.mode === "LIVE"
|
||
? runtime.live_ready
|
||
? "LIVE 就绪"
|
||
: `未就绪(${runtime.live_ready_reason})`
|
||
: "SIM"}
|
||
。有持仓时不可切换模式。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="settings-section">
|
||
<h3>OKX API</h3>
|
||
<div className="settings-fields">
|
||
<div className="field">
|
||
<label htmlFor="okxKey">API Key</label>
|
||
<input
|
||
id="okxKey"
|
||
className="mono"
|
||
type="password"
|
||
autoComplete="off"
|
||
placeholder={
|
||
runtime?.okx_api_key_masked
|
||
? `已配置 ${runtime.okx_api_key_masked}`
|
||
: "未配置"
|
||
}
|
||
value={okxKey}
|
||
onChange={(e) => setOkxKey(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="okxSecret">Secret</label>
|
||
<input
|
||
id="okxSecret"
|
||
className="mono"
|
||
type="password"
|
||
autoComplete="off"
|
||
placeholder={
|
||
runtime?.okx_api_secret_masked
|
||
? `已配置 ${runtime.okx_api_secret_masked}`
|
||
: "未配置"
|
||
}
|
||
value={okxSecret}
|
||
onChange={(e) => setOkxSecret(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="okxPass">Passphrase</label>
|
||
<input
|
||
id="okxPass"
|
||
className="mono"
|
||
type="password"
|
||
autoComplete="off"
|
||
placeholder={
|
||
runtime?.okx_api_passphrase_masked
|
||
? `已配置 ${runtime.okx_api_passphrase_masked}`
|
||
: "未配置"
|
||
}
|
||
value={okxPass}
|
||
onChange={(e) => setOkxPass(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="settings-section">
|
||
<h3>币安 API</h3>
|
||
<div className="settings-fields">
|
||
<div className="field">
|
||
<label htmlFor="bnKey">API Key</label>
|
||
<input
|
||
id="bnKey"
|
||
className="mono"
|
||
type="password"
|
||
autoComplete="off"
|
||
placeholder={
|
||
runtime?.binance_api_key_masked
|
||
? `已配置 ${runtime.binance_api_key_masked}`
|
||
: "未配置"
|
||
}
|
||
value={bnKey}
|
||
onChange={(e) => setBnKey(e.target.value)}
|
||
/>
|
||
</div>
|
||
<div className="field">
|
||
<label htmlFor="bnSecret">Secret</label>
|
||
<input
|
||
id="bnSecret"
|
||
className="mono"
|
||
type="password"
|
||
autoComplete="off"
|
||
placeholder={
|
||
runtime?.binance_api_secret_masked
|
||
? `已配置 ${runtime.binance_api_secret_masked}`
|
||
: "未配置"
|
||
}
|
||
value={bnSecret}
|
||
onChange={(e) => setBnSecret(e.target.value)}
|
||
/>
|
||
</div>
|
||
<p className="settings-hint">
|
||
留空保存=不覆盖已有密钥。交易所选币安且 LIVE 时用此密钥真下单(无
|
||
Passphrase)。
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div className="settings-actions">
|
||
<button className="btn" type="submit" disabled={loading}>
|
||
{loading ? "保存中…" : "保存模式与密钥"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
) : null}
|
||
|
||
{tab === "account" ? (
|
||
<div className="card settings-card settings-card-narrow">
|
||
{err ? <div className="err">{err}</div> : null}
|
||
{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="settings-actions">
|
||
<button className="btn" type="submit" disabled={loading}>
|
||
{loading ? "保存中…" : "保存账号"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|