ui优化
This commit is contained in:
@@ -1,102 +1,655 @@
|
||||
:root {
|
||||
--bg: #0f1216;
|
||||
--panel: #171b22;
|
||||
--text: #e8eaed;
|
||||
--muted: #8b929a;
|
||||
--border: #2a313c;
|
||||
--bg: #0c0e12;
|
||||
--bg-elevated: #12161d;
|
||||
--panel: #181d26;
|
||||
--panel-hover: #1e2430;
|
||||
--text: #e6edf3;
|
||||
--muted: #8b949e;
|
||||
--border: #30363d;
|
||||
--border-soft: #21262d;
|
||||
--green: #3fb950;
|
||||
--red: #f85149;
|
||||
--accent: #58a6ff;
|
||||
--accent: #539bf5;
|
||||
--accent-dim: #1f3a5f;
|
||||
--radius: 10px;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.35);
|
||||
--font: "Segoe UI", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
--mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, "Segoe UI", sans-serif;
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
a { color: var(--accent); }
|
||||
.top-nav {
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* —— 顶栏 —— */
|
||||
.app-shell {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 48px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #12161c;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
.brand span {
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
background: var(--bg-elevated);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.top-nav a {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
padding: 8px 18px;
|
||||
border-radius: 7px;
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.top-nav a.active { background: var(--panel); color: var(--text); border: 1px solid var(--border); }
|
||||
.page { max-width: 1200px; margin: 0 auto; padding: 16px 20px 40px; }
|
||||
.page.hidden { display: none; }
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 14px; }
|
||||
button, .btn {
|
||||
|
||||
.top-nav a:hover {
|
||||
color: var(--text);
|
||||
background: var(--panel-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-nav a.active {
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
/* —— 页面 —— */
|
||||
.page.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: 20px 0 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hint-box {
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-elevated);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hint-box summary {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
}
|
||||
button:hover { border-color: var(--accent); }
|
||||
button.danger { border-color: var(--red); color: var(--red); }
|
||||
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.hint-box summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.hint-box summary::before {
|
||||
content: "▸ ";
|
||||
color: var(--accent);
|
||||
}
|
||||
.hint-box[open] summary::before {
|
||||
content: "▾ ";
|
||||
}
|
||||
|
||||
.hint-box .hint-body {
|
||||
padding: 0 14px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
.hint-box .hint-body code {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
background: var(--panel);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
color: #b8c4ff;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
min-width: 8px;
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* —— 按钮 —— */
|
||||
button,
|
||||
.btn {
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
background: var(--panel-hover);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
border-color: rgba(248, 81, 73, 0.5);
|
||||
color: var(--red);
|
||||
background: rgba(248, 81, 73, 0.08);
|
||||
}
|
||||
|
||||
button.danger:hover:not(:disabled) {
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--accent);
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn-link:hover {
|
||||
background: var(--accent-dim);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chk-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* —— 卡片 —— */
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #3d444d;
|
||||
}
|
||||
|
||||
.card-head {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, transparent 100%);
|
||||
}
|
||||
.card-body { padding: 10px 12px; }
|
||||
.grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||
.form-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||||
.form-row input, .form-row select, .form-row textarea {
|
||||
background: #0d1117;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px;
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.grid-monitor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 监控统计 */
|
||||
.stat-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #b8c4ff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin: 14px 0 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.section-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.data-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-line {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed var(--border-soft);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.list-line:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.pnl-pos {
|
||||
color: var(--green);
|
||||
}
|
||||
.pnl-neg {
|
||||
color: var(--red);
|
||||
}
|
||||
.err {
|
||||
color: var(--red);
|
||||
font-size: 13px;
|
||||
}
|
||||
.form-row input { min-width: 100px; }
|
||||
.rule-tip { font-size: 12px; color: var(--muted); margin: 8px 0; line-height: 1.5; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
th, td { padding: 6px 8px; border-top: 1px solid var(--border); text-align: left; }
|
||||
th { color: var(--muted); }
|
||||
.pnl-pos { color: var(--green); }
|
||||
.pnl-neg { color: var(--red); }
|
||||
.err { color: var(--red); }
|
||||
.badge { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: #1f3a5a; color: #8fc8ff; }
|
||||
.tabs { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.tabs button.active { border-color: var(--accent); color: var(--accent); }
|
||||
#toast {
|
||||
position: fixed; bottom: 16px; right: 16px;
|
||||
max-width: min(480px, 90vw);
|
||||
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-dim);
|
||||
color: #8fc8ff;
|
||||
border: 1px solid rgba(83, 155, 245, 0.35);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* —— 下单区 —— */
|
||||
.trade-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
display: none;
|
||||
z-index: 30;
|
||||
white-space: pre-wrap;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.trade-bar label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.trade-bar select {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
padding: 4px;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-soft);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 8px 16px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: var(--panel);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.trade-meta {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-elevated);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.form-panel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-panel .card-head strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.trade-bar select,
|
||||
.form-row input,
|
||||
.form-row select {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
padding: 9px 11px;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(83, 155, 245, 0.2);
|
||||
}
|
||||
|
||||
.field-check {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.field-check label {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* —— 系统设置 —— */
|
||||
.settings-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settings-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.settings-card-head .ex-name {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
color: var(--text);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-grid .field input {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cap-chips {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.cap-chips label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.settings-card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.settings-card-foot .field {
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
max-width: min(420px, 92vw);
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
display: none;
|
||||
z-index: 50;
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-shell {
|
||||
padding: 0 12px 32px;
|
||||
}
|
||||
.grid-monitor {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
#toast.show { display: block; }
|
||||
.settings-table input { width: 100%; min-width: 80px; }
|
||||
.chk-row { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; }
|
||||
|
||||
@@ -112,67 +112,74 @@
|
||||
inner = `<div class="err">${esc(row.error || "子代理不可用")}</div>`;
|
||||
} else if (!agOk) {
|
||||
inner = `<div class="err">${esc(agErr || "子代理返回失败")}</div>`;
|
||||
inner += `<div class="rule-tip">请检查:PM2 子代理是否在对应 crypto_monitor_* 目录加载了 .env;<code>curl ${esc(row.agent_url || "")}/status</code></div>`;
|
||||
inner += `<div class="empty-hint">请检查 PM2 子代理与 <code>${esc(row.agent_url || "")}/status</code></div>`;
|
||||
} else {
|
||||
const posRows = pos
|
||||
.map(
|
||||
(x) =>
|
||||
`<tr><td>${esc(x.symbol)}</td><td>${esc(x.side)}</td><td>${fmt(x.contracts, 4)}</td><td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
inner = `<div class="rule-tip">余额 ${fmt(ag.balance_usdt, 2)} U · 浮盈合计 <span class="${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</span></div>`;
|
||||
inner += pos.length
|
||||
? `<table><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th></tr>${posRows}</table>`
|
||||
: `<div style="color:var(--muted);padding:6px 0">交易所无持仓</div>`;
|
||||
inner = `<div class="stat-row">
|
||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 4)}</div></div>
|
||||
</div>`;
|
||||
inner += `<div class="section-title">交易所持仓</div>`;
|
||||
if (pos.length) {
|
||||
const posRows = pos
|
||||
.map(
|
||||
(x) =>
|
||||
`<tr><td>${esc(x.symbol)}</td><td>${esc(x.side)}</td><td>${fmt(x.contracts, 4)}</td><td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td></tr>`
|
||||
)
|
||||
.join("");
|
||||
inner += `<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th></tr></thead><tbody>${posRows}</tbody></table>`;
|
||||
} else {
|
||||
inner += `<div class="empty-hint">无持仓</div>`;
|
||||
}
|
||||
if (orders.length) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">机器人持仓 ${orders.length} 笔</div>`;
|
||||
inner += `<div class="section-title">机器人单 · ${orders.length}</div>`;
|
||||
orders.forEach((o) => {
|
||||
inner += `<div class="rule-tip">${esc(o.symbol)} ${o.direction} 成交${o.trigger_price}</div>`;
|
||||
inner += `<div class="list-line">${esc(o.symbol)} · ${esc(o.direction)} · 触发 ${o.trigger_price}</div>`;
|
||||
});
|
||||
}
|
||||
if ((row.capabilities || []).includes("key")) {
|
||||
inner += `<div class="section-title">关键位</div>`;
|
||||
if (!flaskOk) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#f85149">关键位/机器人:策略 Flask 未连通</div>`;
|
||||
const fe = row.flask_error || hm.msg || hm.error || "";
|
||||
const short =
|
||||
fe ||
|
||||
(hm.status === 404
|
||||
? "HTTP 404:请 git pull 并重启各 crypto_* Flask(hub_bridge 路由未注册)"
|
||||
: "请确认实例已启动,且 HUB_BRIDGE_TOKEN 与实例一致或 APP_AUTH_DISABLED=true");
|
||||
inner += `<div class="rule-tip">${esc(short)}</div>`;
|
||||
? "HTTP 404:请重启各 crypto_* Flask"
|
||||
: "策略 Flask 未连通");
|
||||
inner += `<div class="err">${esc(short)}</div>`;
|
||||
} else if (!keys.length) {
|
||||
inner += `<div style="margin-top:8px;color:var(--muted);font-size:12px">关键位:当前无记录(在下单区或实例首页添加)</div>`;
|
||||
inner += `<div class="empty-hint">当前无记录</div>`;
|
||||
} else {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">关键位 ${keys.length} 条</div>`;
|
||||
keys.slice(0, 8).forEach((k) => {
|
||||
const kp = kmap[k.id] || kmap[String(k.id)] || {};
|
||||
const mt = k.monitor_type || k.type || "";
|
||||
inner += `<div class="rule-tip">${esc(k.symbol)} ${esc(mt)} 上${k.upper}/下${k.lower}`;
|
||||
let line = `${esc(k.symbol)} · ${esc(mt)} · ${k.upper} / ${k.lower}`;
|
||||
if (kp.price_display != null || kp.price != null) {
|
||||
inner += ` · 现价 ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
|
||||
line += ` · ${esc(kp.price_display != null ? kp.price_display : kp.price)}`;
|
||||
}
|
||||
inner += ` · 门控 ${esc(kp.gate_summary || "-")}</div>`;
|
||||
line += ` · ${esc(kp.gate_summary || "-")}`;
|
||||
inner += `<div class="list-line">${line}</div>`;
|
||||
});
|
||||
}
|
||||
} else if ((row.capabilities || []).includes("trend")) {
|
||||
inner += `<div style="margin-top:6px;color:var(--muted);font-size:12px">该账户为趋势户,无关键位(见趋势计划或下单区)</div>`;
|
||||
}
|
||||
if (trends.length) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#b8c4ff">趋势计划 ${trends.length} 个运行中</div>`;
|
||||
inner += `<div class="section-title">趋势计划 · ${trends.length}</div>`;
|
||||
trends.forEach((t) => {
|
||||
inner += `<div class="rule-tip">#${t.id} ${esc(t.symbol)} ${t.direction} SL${t.stop_loss} TP${t.take_profit}</div>`;
|
||||
inner += `<div class="list-line">#${t.id} ${esc(t.symbol)} ${t.direction} · SL ${t.stop_loss} · TP ${t.take_profit}</div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
const review = row.review_url
|
||||
? `<a href="${esc(row.review_url)}" target="_blank" rel="noopener" title="打开该实例的交易记录与复盘页(不在中控内操作)">交易复盘</a>`
|
||||
? `<a class="btn-link" href="${esc(row.review_url)}" target="_blank" rel="noopener">复盘</a>`
|
||||
: "";
|
||||
return `<div class="card">
|
||||
<div class="card-head">
|
||||
<div><strong>${esc(row.name)}</strong><div class="rule-tip">${esc(row.flask_url_browser || row.flask_url || "")}</div></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<div>
|
||||
<div class="card-title">${esc(row.name)}</div>
|
||||
<div class="card-sub">${esc(row.flask_url_browser || row.flask_url || "")}</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${review}
|
||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">该户全平</button>
|
||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">全平</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">${inner}</div>
|
||||
@@ -273,16 +280,17 @@
|
||||
const data = await r.json();
|
||||
tradeMeta = data.meta?.meta || data.meta || {};
|
||||
const el = document.getElementById("trade-meta");
|
||||
if (tradeMeta.key_gate_rule_text) {
|
||||
el.textContent = tradeMeta.key_gate_rule_text;
|
||||
} else if (tradeMeta.trend_pullback_preview_ttl) {
|
||||
el.textContent =
|
||||
`预览有效期 ${tradeMeta.trend_pullback_preview_ttl}s · 补仓档 ${tradeMeta.trend_pullback_dca_legs} · 余额偏差≤${tradeMeta.trend_preview_max_drift_pct}%`;
|
||||
} else {
|
||||
el.textContent = "";
|
||||
let txt = "";
|
||||
if (tradeMeta.key_gate_rule_text) txt = tradeMeta.key_gate_rule_text;
|
||||
else if (tradeMeta.trend_pullback_preview_ttl) {
|
||||
txt = `预览 ${tradeMeta.trend_pullback_preview_ttl}s · 补仓 ${tradeMeta.trend_pullback_dca_legs} 档 · 余额偏差 ≤${tradeMeta.trend_preview_max_drift_pct}%`;
|
||||
}
|
||||
el.textContent = txt;
|
||||
el.style.display = txt ? "block" : "none";
|
||||
} catch (e) {
|
||||
document.getElementById("trade-meta").textContent = "";
|
||||
const el = document.getElementById("trade-meta");
|
||||
el.textContent = "";
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,9 +320,9 @@
|
||||
.map((r) => `<tr><td>${r.i}</td><td>${r.price}</td><td>${r.contracts}</td></tr>`)
|
||||
.join("");
|
||||
box.innerHTML = `
|
||||
<div class="rule-tip">预览 #${esc(p.id || trendPreviewId)} 剩余 ${p.expires_in_sec ?? "?"}s</div>
|
||||
<div class="rule-tip">${esc(p.symbol)} ${esc(p.direction)} ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U</div>
|
||||
<table><tr><th>#</th><th>补仓价</th><th>张数</th></tr>${levels}</table>
|
||||
<div class="section-title">预览 #${esc(p.id || trendPreviewId)} · ${p.expires_in_sec ?? "?"}s</div>
|
||||
<div class="list-line">${esc(p.symbol)} ${esc(p.direction)} · ${p.leverage}x · 快照 ${fmt(p.snapshot_available_usdt, 2)} U</div>
|
||||
<table class="data-table"><thead><tr><th>#</th><th>补仓价</th><th>张数</th></tr></thead><tbody>${levels}</tbody></table>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button type="button" id="btn-trend-exec">确认执行(实盘)</button>
|
||||
</div>`;
|
||||
@@ -355,8 +363,8 @@
|
||||
const parts = [];
|
||||
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
|
||||
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
|
||||
if (m.public_origin) parts.push("复盘外链: " + m.public_origin);
|
||||
else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘 127.0.0.1 仅服务器本机可开)");
|
||||
if (m.public_origin) parts.push("浏览器外链基址: " + m.public_origin);
|
||||
else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘链接仅本机可开)");
|
||||
if ((m.env_disabled_ids || []).length)
|
||||
parts.push("环境强制关闭 id: " + m.env_disabled_ids.join(", "));
|
||||
el.textContent = parts.join(" · ");
|
||||
@@ -366,11 +374,11 @@
|
||||
function loadSettingsUI() {
|
||||
loadSettingsMetaLine();
|
||||
loadSettings().then((data) => {
|
||||
const tbody = document.getElementById("settings-tbody");
|
||||
tbody.innerHTML = (data.exchanges || [])
|
||||
.map((ex, idx) => renderSettingsRow(ex, idx))
|
||||
const list = document.getElementById("settings-list");
|
||||
list.innerHTML = (data.exchanges || [])
|
||||
.map((ex, idx) => renderSettingsCard(ex, idx))
|
||||
.join("");
|
||||
tbody.querySelectorAll(".btn-del-ex").forEach((btn) => {
|
||||
list.querySelectorAll(".btn-del-ex").forEach((btn) => {
|
||||
btn.onclick = () => {
|
||||
const i = Number(btn.dataset.idx);
|
||||
data.exchanges.splice(i, 1);
|
||||
@@ -381,46 +389,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderSettingsRow(ex, idx) {
|
||||
function renderSettingsCard(ex, idx) {
|
||||
const caps = ex.capabilities || [];
|
||||
const envOff = ex.env_disabled
|
||||
? ' <span class="badge">环境变量强制关</span>'
|
||||
? '<span class="badge">环境变量强制关</span>'
|
||||
: "";
|
||||
return `<tr data-idx="${idx}" data-key="${esc(ex.key || ex.id || "")}">
|
||||
<td><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/>${envOff}</td>
|
||||
<td><input class="ex-name" value="${esc(ex.name || "")}" /></td>
|
||||
<td><input class="ex-flask" value="${esc(ex.flask_url || "")}" /></td>
|
||||
<td><input class="ex-agent" value="${esc(ex.agent_url || "")}" /></td>
|
||||
<td><input class="ex-review" value="${esc(ex.review_url || "")}" /></td>
|
||||
<td class="chk-row">
|
||||
<label><input type="checkbox" class="cap-order" ${caps.includes("order") ? "checked" : ""}/>下单</label>
|
||||
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/>关键位</label>
|
||||
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/>趋势</label>
|
||||
</td>
|
||||
<td><input class="ex-id" value="${esc(ex.id || "")}" style="width:48px" /></td>
|
||||
<td><button type="button" class="btn-del-ex" data-idx="${idx}">删</button></td>
|
||||
</tr>`;
|
||||
return `<div class="settings-card" data-idx="${idx}" data-key="${esc(ex.key || ex.id || "")}">
|
||||
<div class="settings-card-head">
|
||||
<label class="chk-label"><input type="checkbox" class="ex-enabled" ${ex.enabled ? "checked" : ""} ${ex.env_disabled ? "disabled" : ""}/> 启用</label>
|
||||
${envOff}
|
||||
<input class="ex-name" value="${esc(ex.name || "")}" placeholder="显示名称" />
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="field"><label>Flask URL</label><input class="ex-flask" value="${esc(ex.flask_url || "")}" /></div>
|
||||
<div class="field"><label>Agent URL</label><input class="ex-agent" value="${esc(ex.agent_url || "")}" /></div>
|
||||
<div class="field field-wide"><label>复盘链接(可空)</label><input class="ex-review" value="${esc(ex.review_url || "")}" placeholder="留空则自动生成 /records" /></div>
|
||||
</div>
|
||||
<div class="cap-chips">
|
||||
<label><input type="checkbox" class="cap-order" ${caps.includes("order") ? "checked" : ""}/> 下单</label>
|
||||
<label><input type="checkbox" class="cap-key" ${caps.includes("key") ? "checked" : ""}/> 关键位</label>
|
||||
<label><input type="checkbox" class="cap-trend" ${caps.includes("trend") ? "checked" : ""}/> 趋势</label>
|
||||
</div>
|
||||
<div class="settings-card-foot">
|
||||
<div class="field"><label>id</label><input class="ex-id" value="${esc(ex.id || "")}" /></div>
|
||||
<button type="button" class="danger btn-del-ex" data-idx="${idx}">删除账户</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function collectSettingsFromUI() {
|
||||
const rows = [...document.querySelectorAll("#settings-tbody tr")];
|
||||
const rows = [...document.querySelectorAll("#settings-list .settings-card")];
|
||||
return {
|
||||
version: 1,
|
||||
exchanges: rows.map((tr) => {
|
||||
exchanges: rows.map((card) => {
|
||||
const caps = [];
|
||||
if (tr.querySelector(".cap-order").checked) caps.push("order");
|
||||
if (tr.querySelector(".cap-key").checked) caps.push("key");
|
||||
if (tr.querySelector(".cap-trend").checked) caps.push("trend");
|
||||
const id = tr.querySelector(".ex-id").value.trim();
|
||||
const stableKey = (tr.dataset.key || id).trim();
|
||||
if (card.querySelector(".cap-order").checked) caps.push("order");
|
||||
if (card.querySelector(".cap-key").checked) caps.push("key");
|
||||
if (card.querySelector(".cap-trend").checked) caps.push("trend");
|
||||
const id = card.querySelector(".ex-id").value.trim();
|
||||
const stableKey = (card.dataset.key || id).trim();
|
||||
return {
|
||||
id: id,
|
||||
key: stableKey,
|
||||
name: tr.querySelector(".ex-name").value.trim(),
|
||||
flask_url: tr.querySelector(".ex-flask").value.trim(),
|
||||
agent_url: tr.querySelector(".ex-agent").value.trim(),
|
||||
review_url: tr.querySelector(".ex-review").value.trim(),
|
||||
enabled: tr.querySelector(".ex-enabled").checked,
|
||||
name: card.querySelector(".ex-name").value.trim(),
|
||||
flask_url: card.querySelector(".ex-flask").value.trim(),
|
||||
agent_url: card.querySelector(".ex-agent").value.trim(),
|
||||
review_url: card.querySelector(".ex-review").value.trim(),
|
||||
enabled: card.querySelector(".ex-enabled").checked,
|
||||
capabilities: caps,
|
||||
};
|
||||
}),
|
||||
@@ -471,18 +486,20 @@
|
||||
};
|
||||
document.getElementById("order-sltp-mode").onchange = function () {
|
||||
const pct = this.value === "pct";
|
||||
document.getElementById("order-sl").style.display = pct ? "none" : "";
|
||||
document.getElementById("order-tp").style.display = pct ? "none" : "";
|
||||
document.getElementById("order-sl-pct").style.display = pct ? "" : "none";
|
||||
document.getElementById("order-tp-pct").style.display = pct ? "" : "none";
|
||||
const slField = document.getElementById("order-sl").closest(".field");
|
||||
const tpField = document.getElementById("order-tp").closest(".field");
|
||||
if (slField) slField.style.display = pct ? "none" : "";
|
||||
if (tpField) tpField.style.display = pct ? "none" : "";
|
||||
document.getElementById("wrap-sl-pct").style.display = pct ? "" : "none";
|
||||
document.getElementById("wrap-tp-pct").style.display = pct ? "" : "none";
|
||||
};
|
||||
document.getElementById("key-sl-tp-mode").onchange = function () {
|
||||
const manual = this.value === "trend_manual";
|
||||
document.getElementById("key-manual-tp").style.display = manual ? "" : "none";
|
||||
document.getElementById("wrap-key-manual-tp").style.display = manual ? "" : "none";
|
||||
};
|
||||
document.getElementById("trend-direction").onchange = function () {
|
||||
const inp = document.getElementById("trend-add-upper");
|
||||
inp.placeholder = this.value === "short" ? "补仓下沿价" : "补仓上沿价";
|
||||
const lbl = document.getElementById("trend-add-label");
|
||||
if (lbl) lbl.textContent = this.value === "short" ? "补仓下沿价" : "补仓上沿价";
|
||||
};
|
||||
document.getElementById("btn-settings-save").onclick = saveSettings;
|
||||
document.getElementById("btn-settings-reload").onclick = loadSettingsUI;
|
||||
|
||||
@@ -7,156 +7,237 @@
|
||||
<link rel="stylesheet" href="/assets/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="top-nav">
|
||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||
<a href="/trade" id="nav-trade">下单区</a>
|
||||
<a href="/settings" id="nav-settings">系统设置</a>
|
||||
</nav>
|
||||
<div class="app-shell">
|
||||
<header class="app-header">
|
||||
<div class="brand">交易中控 <span>manual_trading_hub</span></div>
|
||||
<nav class="top-nav">
|
||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||
<a href="/trade" id="nav-trade">下单区</a>
|
||||
<a href="/settings" id="nav-settings">系统设置</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div id="page-monitor" class="page">
|
||||
<h1>监控区</h1>
|
||||
<p class="rule-tip" style="margin-top:0">
|
||||
持仓/余额来自子代理;关键位、机器人单来自各实例 Flask(须 PM2 跑着 crypto_*)。
|
||||
「交易复盘」在新标签打开 /records。其它电脑访问中控时,请在 hub 的 .env 设置
|
||||
HUB_PUBLIC_ORIGIN=http://Ubuntu内网IP,否则会跳到 127.0.0.1。
|
||||
</p>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-monitor-refresh">立即刷新</button>
|
||||
<label style="color:var(--muted);font-size:12px;display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" id="auto-monitor" checked /> 每 5 秒刷新
|
||||
</label>
|
||||
<button type="button" id="btn-close-all" class="danger">全局紧急全平</button>
|
||||
<span id="monitor-updated" style="color:var(--muted);font-size:12px"></span>
|
||||
<div id="page-monitor" class="page">
|
||||
<div class="page-head">
|
||||
<h1>监控区</h1>
|
||||
</div>
|
||||
<details class="hint-box">
|
||||
<summary>说明:数据来源与复盘链接</summary>
|
||||
<div class="hint-body">
|
||||
持仓与余额来自子代理;关键位、机器人单、趋势计划来自各实例 Flask(须 PM2 运行 crypto_*)。<br />
|
||||
「交易复盘」在新标签打开该实例 /records。其它电脑访问中控时,请在 hub 的 <code>.env</code> 设置
|
||||
<code>HUB_PUBLIC_ORIGIN=http://服务器内网IP</code>。
|
||||
</div>
|
||||
</details>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-monitor-refresh">立即刷新</button>
|
||||
<label class="chk-label">
|
||||
<input type="checkbox" id="auto-monitor" checked /> 每 5 秒自动刷新
|
||||
</label>
|
||||
<button type="button" id="btn-close-all" class="danger">全局紧急全平</button>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<span id="monitor-updated" class="toolbar-meta"></span>
|
||||
</div>
|
||||
<div id="monitor-grid" class="grid-monitor"></div>
|
||||
</div>
|
||||
<div id="monitor-grid" class="grid-2"></div>
|
||||
</div>
|
||||
|
||||
<div id="page-trade" class="page hidden">
|
||||
<h1>下单区</h1>
|
||||
<div class="form-row">
|
||||
<label>账户</label>
|
||||
<select id="trade-account"></select>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button type="button" data-tab="order" class="active">人工下单</button>
|
||||
<button type="button" data-tab="key">关键位</button>
|
||||
<button type="button" data-tab="trend">趋势回调</button>
|
||||
</div>
|
||||
<div id="trade-meta" class="rule-tip"></div>
|
||||
<div id="page-trade" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1>下单区</h1>
|
||||
</div>
|
||||
<div class="trade-bar">
|
||||
<label for="trade-account">交易账户</label>
|
||||
<select id="trade-account"></select>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<button type="button" data-tab="order" class="active">人工下单</button>
|
||||
<button type="button" data-tab="key">关键位</button>
|
||||
<button type="button" data-tab="trend">趋势回调</button>
|
||||
</div>
|
||||
<div id="trade-meta" class="trade-meta" style="display:none"></div>
|
||||
|
||||
<div id="panel-order" class="card">
|
||||
<div class="card-head"><strong>人工下单</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-order" class="form-row">
|
||||
<input name="symbol" placeholder="BTC 或 BTC/USDT" required />
|
||||
<select name="direction" required>
|
||||
<option value="">方向</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
<select name="sltp_mode" id="order-sltp-mode">
|
||||
<option value="price">止盈止损:价格</option>
|
||||
<option value="pct">止盈止损:百分比</option>
|
||||
</select>
|
||||
<select name="trade_style" required>
|
||||
<option value="trend">趋势单</option>
|
||||
<option value="swing">波段单</option>
|
||||
</select>
|
||||
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)" />
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--muted)">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" checked /> 移动保本
|
||||
</label>
|
||||
<input name="sl" id="order-sl" step="any" placeholder="止损价" required />
|
||||
<input name="tgt" id="order-tp" step="any" placeholder="止盈价" required />
|
||||
<input name="sl_pct" id="order-sl-pct" type="number" step="0.01" placeholder="止损%" style="display:none" />
|
||||
<input name="tp_pct" id="order-tp-pct" type="number" step="0.01" placeholder="止盈%" style="display:none" />
|
||||
<button type="submit">开仓(以损定仓)</button>
|
||||
</form>
|
||||
<div id="panel-order" class="form-panel card">
|
||||
<div class="card-head"><strong>人工下单</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-order" class="form-grid">
|
||||
<div class="field">
|
||||
<label>合约</label>
|
||||
<input name="symbol" placeholder="BTC / BTCUSDT" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>方向</label>
|
||||
<select name="direction" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>止盈止损</label>
|
||||
<select name="sltp_mode" id="order-sltp-mode">
|
||||
<option value="price">价格</option>
|
||||
<option value="pct">百分比</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>单型</label>
|
||||
<select name="trade_style" required>
|
||||
<option value="trend">趋势单</option>
|
||||
<option value="swing">波段单</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>杠杆</label>
|
||||
<input name="leverage" type="number" min="1" step="1" placeholder="可选" />
|
||||
</div>
|
||||
<div class="field field-check">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" id="order-be" checked />
|
||||
<label for="order-be">移动保本</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>止损</label>
|
||||
<input name="sl" id="order-sl" step="any" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>止盈</label>
|
||||
<input name="tgt" id="order-tp" step="any" required />
|
||||
</div>
|
||||
<div class="field" id="wrap-sl-pct" style="display:none">
|
||||
<label>止损 %</label>
|
||||
<input name="sl_pct" id="order-sl-pct" type="number" step="0.01" />
|
||||
</div>
|
||||
<div class="field" id="wrap-tp-pct" style="display:none">
|
||||
<label>止盈 %</label>
|
||||
<input name="tp_pct" id="order-tp-pct" type="number" step="0.01" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="primary">开仓(以损定仓)</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-key" class="form-panel card hidden">
|
||||
<div class="card-head"><strong>添加关键位</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-key" class="form-grid">
|
||||
<div class="field">
|
||||
<label>合约</label>
|
||||
<input name="symbol" placeholder="BTC / BTCUSDT" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>类型</label>
|
||||
<select name="type" required>
|
||||
<option value="箱体突破">箱体突破</option>
|
||||
<option value="收敛突破">收敛突破</option>
|
||||
<option value="斐波回调0.618">斐波回调0.618</option>
|
||||
<option value="斐波回调0.786">斐波回调0.786</option>
|
||||
<option value="关键阻力位">关键阻力位</option>
|
||||
<option value="关键支撑位">关键支撑位</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>方向</label>
|
||||
<select name="direction" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>上沿 / 阻力</label>
|
||||
<input name="upper" step="any" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>下沿 / 支撑</label>
|
||||
<input name="lower" step="any" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模式</label>
|
||||
<select name="sl_tp_mode" id="key-sl-tp-mode">
|
||||
<option value="standard">标准突破</option>
|
||||
<option value="box_1p5">箱体1R·止盈1.5H</option>
|
||||
<option value="trend_manual">趋势单·自填止盈</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" id="wrap-key-manual-tp" style="display:none">
|
||||
<label>趋势止盈价</label>
|
||||
<input name="manual_take_profit" id="key-manual-tp" step="any" />
|
||||
</div>
|
||||
<div class="field field-check">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" id="key-be-cb" />
|
||||
<label for="key-be-cb">移动保本</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="primary">添加关键位</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-trend" class="form-panel card hidden">
|
||||
<div class="card-head"><strong>趋势回调</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-trend" class="form-grid">
|
||||
<div class="field">
|
||||
<label>合约</label>
|
||||
<input name="symbol" placeholder="BTC / ETHUSDT" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>方向</label>
|
||||
<select name="direction" id="trend-direction" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>杠杆</label>
|
||||
<input name="leverage" type="number" min="1" step="1" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>风险 %</label>
|
||||
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>止损价</label>
|
||||
<input name="sl" step="any" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label id="trend-add-label">补仓上沿价</label>
|
||||
<input name="add_upper" id="trend-add-upper" step="any" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>止盈价</label>
|
||||
<input name="take_profit" step="any" required />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="primary">生成预览</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="trend-preview-box" style="margin-top:16px;display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-key" class="card hidden">
|
||||
<div class="card-head"><strong>添加关键位</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-key" class="form-row">
|
||||
<input name="symbol" placeholder="BTC 或 BTC/USDT" required />
|
||||
<select name="type" required>
|
||||
<option value="箱体突破">箱体突破</option>
|
||||
<option value="收敛突破">收敛突破</option>
|
||||
<option value="斐波回调0.618">斐波回调0.618</option>
|
||||
<option value="斐波回调0.786">斐波回调0.786</option>
|
||||
<option value="关键阻力位">关键阻力位</option>
|
||||
<option value="关键支撑位">关键支撑位</option>
|
||||
</select>
|
||||
<select name="direction" required>
|
||||
<option value="">方向</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
<input name="upper" step="any" placeholder="上沿/阻力" required />
|
||||
<input name="lower" step="any" placeholder="下沿/支撑" required />
|
||||
<select name="sl_tp_mode" id="key-sl-tp-mode">
|
||||
<option value="standard">标准突破</option>
|
||||
<option value="box_1p5">箱体1R·止盈1.5H</option>
|
||||
<option value="trend_manual">趋势单·自填止盈</option>
|
||||
</select>
|
||||
<input name="manual_take_profit" id="key-manual-tp" step="any" placeholder="趋势单止盈价" style="display:none" />
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--muted)">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" id="key-be-cb" /> 移动保本
|
||||
</label>
|
||||
<button type="submit">添加关键位</button>
|
||||
</form>
|
||||
<div id="page-settings" class="page hidden">
|
||||
<div class="page-head">
|
||||
<h1>系统设置</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panel-trend" class="card hidden">
|
||||
<div class="card-head"><strong>趋势回调</strong></div>
|
||||
<div class="card-body">
|
||||
<form id="form-trend" class="form-row">
|
||||
<input name="symbol" placeholder="BTC 或 ETH/USDT" required />
|
||||
<select name="direction" id="trend-direction" required>
|
||||
<option value="">方向</option>
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
<input name="leverage" type="number" min="1" step="1" placeholder="杠杆" required />
|
||||
<input name="risk_percent" type="number" min="0.1" step="0.1" value="5" placeholder="风险%" />
|
||||
<input name="sl" step="any" placeholder="止损价" required />
|
||||
<input name="add_upper" id="trend-add-upper" step="any" placeholder="补仓上沿价" required />
|
||||
<input name="take_profit" step="any" placeholder="止盈价" required />
|
||||
<button type="submit">生成预览</button>
|
||||
</form>
|
||||
<div id="trend-preview-box" style="margin-top:12px;display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-settings" class="page hidden">
|
||||
<h1>系统设置</h1>
|
||||
<p class="rule-tip">
|
||||
配置各交易所 Flask 地址与子代理地址,点击「保存设置」写入本目录
|
||||
<code>hub_settings.json</code>(重启 hub 后仍生效)。OKX 默认关闭;环境变量
|
||||
<code>HUB_DISABLED_IDS=1</code> 会强制关闭对应 id(勾选框灰掉)。实例须配置与中控一致的
|
||||
<code>HUB_BRIDGE_TOKEN</code>,或本机调试时 <code>APP_AUTH_DISABLED=true</code>。
|
||||
</p>
|
||||
<p id="settings-meta-line" class="rule-tip"></p>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-settings-save">保存设置</button>
|
||||
<button type="button" id="btn-settings-add">添加交易所</button>
|
||||
<button type="button" id="btn-settings-reload">重新加载</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body" style="overflow:auto">
|
||||
<table class="settings-table" id="settings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>启用</th><th>显示名</th><th>Flask URL</th><th>Agent URL</th><th>复盘链接</th>
|
||||
<th>能力</th><th>id</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="settings-tbody"></tbody>
|
||||
</table>
|
||||
<details class="hint-box">
|
||||
<summary>配置说明</summary>
|
||||
<div class="hint-body">
|
||||
保存后写入 <code>hub_settings.json</code>。Flask / Agent 填本机地址即可;复盘链接可留空(由 Flask 地址自动生成)。<br />
|
||||
<code>HUB_DISABLED_IDS</code> 可强制关闭账户;<code>HUB_BRIDGE_TOKEN</code> 与实例一致,或实例 <code>APP_AUTH_DISABLED=true</code>。
|
||||
</div>
|
||||
</details>
|
||||
<p id="settings-meta-line" class="trade-meta"></p>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-settings-save" class="primary">保存设置</button>
|
||||
<button type="button" id="btn-settings-add">添加交易所</button>
|
||||
<button type="button" id="btn-settings-reload">重新加载</button>
|
||||
</div>
|
||||
<div id="settings-list" class="settings-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user