feat: right-click context menu and Delete key for chart drawings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2928,6 +2928,72 @@ body.login-page {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-draw-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1200;
|
||||||
|
min-width: 168px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--panel-bg, #1a1f2e);
|
||||||
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-head {
|
||||||
|
padding: 6px 12px 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-family: var(--font);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-item:hover:not(:disabled) {
|
||||||
|
background: var(--inset-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-item.is-danger {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-sep {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-draw-menu-kbd {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--inset-surface);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
.market-exchange-badge {
|
.market-exchange-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -64,6 +64,9 @@
|
|||||||
let dragActive = false;
|
let dragActive = false;
|
||||||
let dragStartPx = null;
|
let dragStartPx = null;
|
||||||
let pathPreviewPt = null;
|
let pathPreviewPt = null;
|
||||||
|
let menuEl = null;
|
||||||
|
let unsubClick = null;
|
||||||
|
let mainBound = false;
|
||||||
|
|
||||||
function uid() {
|
function uid() {
|
||||||
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||||
@@ -685,18 +688,25 @@
|
|||||||
const h = hostEl.clientHeight;
|
const h = hostEl.clientHeight;
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
drawings.forEach(function (d) {
|
drawings.forEach(function (d) {
|
||||||
|
if (d.hidden) ctx.globalAlpha = 0.14;
|
||||||
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
||||||
|
if (d.hidden) ctx.globalAlpha = 1;
|
||||||
});
|
});
|
||||||
if (draft) {
|
if (draft) {
|
||||||
const preview = draft.type === "path" ? pathPreviewPt : null;
|
const preview = draft.type === "path" ? pathPreviewPt : null;
|
||||||
renderDrawing(ctx, draft, w, h, true, preview);
|
renderDrawing(ctx, draft, w, h, true, preview);
|
||||||
}
|
}
|
||||||
|
if (activeTool === "cursor" && selectedId) {
|
||||||
|
const sel = getDrawingById(selectedId);
|
||||||
|
if (sel) drawSelectionOverlay(ctx, sel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitDrawing(d) {
|
function commitDrawing(d) {
|
||||||
if (!d || !d.type) return;
|
if (!d || !d.type) return;
|
||||||
d.id = uid();
|
d.id = uid();
|
||||||
drawings.push(d);
|
drawings.push(d);
|
||||||
|
selectedId = d.id;
|
||||||
draft = null;
|
draft = null;
|
||||||
pathPreviewPt = null;
|
pathPreviewPt = null;
|
||||||
saveDrawings();
|
saveDrawings();
|
||||||
@@ -719,17 +729,222 @@
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryEraseAt(x, y) {
|
function getDrawingById(id) {
|
||||||
|
for (let i = 0; i < drawings.length; i++) {
|
||||||
|
if (drawings[i].id === id) return drawings[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDrawingAt(x, y) {
|
||||||
for (let i = drawings.length - 1; i >= 0; i--) {
|
for (let i = drawings.length - 1; i >= 0; i--) {
|
||||||
if (hitTestDrawing(drawings[i], x, y)) {
|
if (hitTestDrawing(drawings[i], x, y)) return drawings[i];
|
||||||
drawings.splice(i, 1);
|
}
|
||||||
selectedId = null;
|
return null;
|
||||||
saveDrawings();
|
}
|
||||||
scheduleRedraw();
|
|
||||||
return true;
|
function selectDrawing(id) {
|
||||||
|
selectedId = id || null;
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deselectDrawing() {
|
||||||
|
if (!selectedId) return;
|
||||||
|
selectedId = null;
|
||||||
|
hideContextMenu();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDrawing(id, opts) {
|
||||||
|
if (!id) return;
|
||||||
|
const force = !!(opts && opts.force);
|
||||||
|
const d = getDrawingById(id);
|
||||||
|
if (!d) return;
|
||||||
|
if (d.locked && !force) return;
|
||||||
|
drawings = drawings.filter(function (item) {
|
||||||
|
return item.id !== id;
|
||||||
|
});
|
||||||
|
if (selectedId === id) selectedId = null;
|
||||||
|
hideContextMenu();
|
||||||
|
saveDrawings();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelectedDrawing() {
|
||||||
|
if (!selectedId) return;
|
||||||
|
removeDrawing(selectedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneDrawing(id) {
|
||||||
|
const src = getDrawingById(id);
|
||||||
|
if (!src || src.locked) return;
|
||||||
|
const copy = JSON.parse(JSON.stringify(src));
|
||||||
|
copy.id = uid();
|
||||||
|
copy.locked = false;
|
||||||
|
const candles = getCandles();
|
||||||
|
const timeStep = candles.length > 1 ? Math.abs(candles[1].time - candles[0].time) : 60;
|
||||||
|
copy.points = (copy.points || []).map(function (p, idx) {
|
||||||
|
return {
|
||||||
|
time: p.time + timeStep * (idx + 1),
|
||||||
|
price: p.price * 1.001,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (copy.text) copy.text = String(copy.text);
|
||||||
|
drawings.push(copy);
|
||||||
|
selectedId = copy.id;
|
||||||
|
saveDrawings();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDrawingLock(id) {
|
||||||
|
const d = getDrawingById(id);
|
||||||
|
if (!d) return;
|
||||||
|
d.locked = !d.locked;
|
||||||
|
saveDrawings();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDrawingHide(id) {
|
||||||
|
const d = getDrawingById(id);
|
||||||
|
if (!d) return;
|
||||||
|
d.hidden = !d.hidden;
|
||||||
|
if (d.hidden && selectedId === id) selectedId = null;
|
||||||
|
hideContextMenu();
|
||||||
|
saveDrawings();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTypingTarget(el) {
|
||||||
|
if (!el) return false;
|
||||||
|
const tag = (el.tagName || "").toUpperCase();
|
||||||
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || !!el.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureContextMenu() {
|
||||||
|
if (menuEl) return menuEl;
|
||||||
|
menuEl = document.createElement("div");
|
||||||
|
menuEl.id = "market-draw-menu";
|
||||||
|
menuEl.className = "market-draw-menu hidden";
|
||||||
|
menuEl.setAttribute("role", "menu");
|
||||||
|
document.body.appendChild(menuEl);
|
||||||
|
menuEl.addEventListener("click", function (ev) {
|
||||||
|
const btn = ev.target.closest("[data-action]");
|
||||||
|
if (!btn || btn.disabled) return;
|
||||||
|
const action = btn.getAttribute("data-action");
|
||||||
|
const id = menuEl._targetId;
|
||||||
|
if (!id) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (action === "clone") cloneDrawing(id);
|
||||||
|
else if (action === "toggle-lock") toggleDrawingLock(id);
|
||||||
|
else if (action === "toggle-hide") toggleDrawingHide(id);
|
||||||
|
else if (action === "remove") removeDrawing(id, { force: true });
|
||||||
|
if (action !== "remove" && action !== "toggle-hide") {
|
||||||
|
const d = getDrawingById(id);
|
||||||
|
if (d) showContextMenu(menuEl._clientX, menuEl._clientY, d);
|
||||||
|
else hideContextMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("pointerdown", function (ev) {
|
||||||
|
if (!menuEl || menuEl.classList.contains("hidden")) return;
|
||||||
|
if (!menuEl.contains(ev.target)) hideContextMenu();
|
||||||
|
});
|
||||||
|
return menuEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideContextMenu() {
|
||||||
|
if (!menuEl) return;
|
||||||
|
menuEl.classList.add("hidden");
|
||||||
|
menuEl._targetId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showContextMenu(clientX, clientY, d) {
|
||||||
|
if (!d) return;
|
||||||
|
const menu = ensureContextMenu();
|
||||||
|
const label = TOOL_LABELS[d.type] || d.type;
|
||||||
|
const locked = !!d.locked;
|
||||||
|
menu.innerHTML =
|
||||||
|
'<div class="market-draw-menu-head">' +
|
||||||
|
label +
|
||||||
|
"</div>" +
|
||||||
|
'<button type="button" class="market-draw-menu-item" data-action="clone"' +
|
||||||
|
(locked ? " disabled" : "") +
|
||||||
|
">克隆</button>" +
|
||||||
|
'<button type="button" class="market-draw-menu-item" data-action="toggle-lock">' +
|
||||||
|
(locked ? "解锁" : "锁定") +
|
||||||
|
"</button>" +
|
||||||
|
'<button type="button" class="market-draw-menu-item" data-action="toggle-hide">' +
|
||||||
|
(d.hidden ? "显示" : "隐藏") +
|
||||||
|
"</button>" +
|
||||||
|
'<hr class="market-draw-menu-sep" />' +
|
||||||
|
'<button type="button" class="market-draw-menu-item is-danger" data-action="remove">移除 <span class="market-draw-menu-kbd">Del</span></button>';
|
||||||
|
menu.classList.remove("hidden");
|
||||||
|
menu._targetId = d.id;
|
||||||
|
menu._clientX = clientX;
|
||||||
|
menu._clientY = clientY;
|
||||||
|
menu.style.visibility = "hidden";
|
||||||
|
menu.style.left = "0px";
|
||||||
|
menu.style.top = "0px";
|
||||||
|
const mw = menu.offsetWidth;
|
||||||
|
const mh = menu.offsetHeight;
|
||||||
|
const pad = 8;
|
||||||
|
let left = clientX;
|
||||||
|
let top = clientY;
|
||||||
|
if (left + mw > window.innerWidth - pad) left = window.innerWidth - mw - pad;
|
||||||
|
if (top + mh > window.innerHeight - pad) top = window.innerHeight - mh - pad;
|
||||||
|
if (left < pad) left = pad;
|
||||||
|
if (top < pad) top = pad;
|
||||||
|
menu.style.left = left + "px";
|
||||||
|
menu.style.top = top + "px";
|
||||||
|
menu.style.visibility = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSelectionOverlay(ctx, d) {
|
||||||
|
if (!d) return;
|
||||||
|
const pts = d.points || [];
|
||||||
|
const handleColor = d.locked ? "#94a3b8" : "#2962ff";
|
||||||
|
pts.forEach(function (p, idx) {
|
||||||
|
const x = timeToX(p.time);
|
||||||
|
const y = priceToY(p.price);
|
||||||
|
if (x == null || y == null) return;
|
||||||
|
const large = d.type === "path" && idx === pts.length - 1;
|
||||||
|
drawHandle(ctx, x, y, large, handleColor);
|
||||||
|
});
|
||||||
|
if ((d.type === "trend" || d.type === "channel") && pts.length >= 2) {
|
||||||
|
const x1 = timeToX(pts[0].time);
|
||||||
|
const y1 = priceToY(pts[0].price);
|
||||||
|
const x2 = timeToX(pts[1].time);
|
||||||
|
const y2 = priceToY(pts[1].price);
|
||||||
|
if (x1 != null && y1 != null && x2 != null && y2 != null) {
|
||||||
|
const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
|
||||||
|
const text = angle.toFixed(2) + "°";
|
||||||
|
ctx.font = "11px sans-serif";
|
||||||
|
const tw = ctx.measureText(text).width;
|
||||||
|
const bx = x2 + 10;
|
||||||
|
const by = y2 - 8;
|
||||||
|
ctx.fillStyle = "rgba(15, 23, 42, 0.88)";
|
||||||
|
ctx.fillRect(bx - 4, by - 12, tw + 8, 18);
|
||||||
|
ctx.fillStyle = "#f8fafc";
|
||||||
|
ctx.fillText(text, bx, by);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
if (d.locked) {
|
||||||
|
const anchor = pts[0];
|
||||||
|
if (!anchor) return;
|
||||||
|
const ax = timeToX(anchor.time);
|
||||||
|
const ay = priceToY(anchor.price);
|
||||||
|
if (ax == null || ay == null) return;
|
||||||
|
ctx.font = "10px sans-serif";
|
||||||
|
ctx.fillStyle = "#94a3b8";
|
||||||
|
ctx.fillText("已锁定", ax + 8, ay - 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryEraseAt(x, y) {
|
||||||
|
const d = pickDrawingAt(x, y);
|
||||||
|
if (!d || d.locked) return false;
|
||||||
|
removeDrawing(d.id);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPointerDown(ev) {
|
function onPointerDown(ev) {
|
||||||
@@ -891,9 +1106,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onContextMenu(ev) {
|
function onContextMenu(ev) {
|
||||||
if (activeTool !== "path" || !draft || draft.type !== "path") return;
|
if (activeTool === "path" && draft && draft.type === "path") {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
finishPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMainContextMenu(ev) {
|
||||||
|
if (!hostEl) return;
|
||||||
|
if (activeTool === "path" && draft && draft.type === "path") return;
|
||||||
|
if (draft || dragActive) return;
|
||||||
|
const rect = hostEl.getBoundingClientRect();
|
||||||
|
const x = ev.clientX - rect.left;
|
||||||
|
const y = ev.clientY - rect.top;
|
||||||
|
const d = pickDrawingAt(x, y);
|
||||||
|
if (!d) {
|
||||||
|
hideContextMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
finishPath();
|
selectDrawing(d.id);
|
||||||
|
showContextMenu(ev.clientX, ev.clientY, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
function distSeg(px, py, x1, y1, x2, y2) {
|
function distSeg(px, py, x1, y1, x2, y2) {
|
||||||
@@ -1041,20 +1275,66 @@
|
|||||||
canvasEl.addEventListener("pointercancel", onPointerUp);
|
canvasEl.addEventListener("pointercancel", onPointerUp);
|
||||||
canvasEl.addEventListener("dblclick", onDblClick);
|
canvasEl.addEventListener("dblclick", onDblClick);
|
||||||
canvasEl.addEventListener("contextmenu", onContextMenu);
|
canvasEl.addEventListener("contextmenu", onContextMenu);
|
||||||
document.addEventListener("keydown", function (ev) {
|
document.addEventListener("keydown", onDrawKeydown);
|
||||||
const page = document.getElementById("page-market");
|
}
|
||||||
if (!page || page.classList.contains("hidden")) return;
|
|
||||||
if (ev.key === "Escape" && (draft || dragActive)) {
|
function onDrawKeydown(ev) {
|
||||||
|
const page = document.getElementById("page-market");
|
||||||
|
if (!page || page.classList.contains("hidden")) return;
|
||||||
|
if (isTypingTarget(ev.target)) return;
|
||||||
|
if (ev.key === "Escape") {
|
||||||
|
if (draft || dragActive) {
|
||||||
cancelDraft();
|
cancelDraft();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.key === "Enter" && activeTool === "path" && draft && draft.type === "path") {
|
if (!menuEl || menuEl.classList.contains("hidden")) {
|
||||||
finishPath();
|
deselectDrawing();
|
||||||
|
} else {
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.key === "Enter" && activeTool === "path" && draft && draft.type === "path") {
|
||||||
|
finishPath();
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(ev.key === "Delete" || ev.key === "Backspace") &&
|
||||||
|
activeTool === "cursor" &&
|
||||||
|
selectedId
|
||||||
|
) {
|
||||||
|
const d = getDrawingById(selectedId);
|
||||||
|
if (d && !d.locked) {
|
||||||
|
removeSelectedDrawing();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindChartClick() {
|
||||||
|
if (!chart || typeof chart.subscribeClick !== "function") return;
|
||||||
|
if (unsubClick) {
|
||||||
|
try {
|
||||||
|
unsubClick();
|
||||||
|
} catch (_) {}
|
||||||
|
unsubClick = null;
|
||||||
|
}
|
||||||
|
unsubClick = chart.subscribeClick(function (param) {
|
||||||
|
if (activeTool !== "cursor" || !param || !param.point) return;
|
||||||
|
hideContextMenu();
|
||||||
|
const d = pickDrawingAt(param.point.x, param.point.y);
|
||||||
|
if (d) selectDrawing(d.id);
|
||||||
|
else deselectDrawing();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindMainEl() {
|
||||||
|
if (!mainEl || mainBound) return;
|
||||||
|
mainBound = true;
|
||||||
|
mainEl.addEventListener("contextmenu", onMainContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
function attach(opts) {
|
function attach(opts) {
|
||||||
chart = opts.chart || null;
|
chart = opts.chart || null;
|
||||||
series = opts.series || null;
|
series = opts.series || null;
|
||||||
@@ -1066,6 +1346,8 @@
|
|||||||
mountCanvasOverlay();
|
mountCanvasOverlay();
|
||||||
bindToolbar();
|
bindToolbar();
|
||||||
bindCanvas();
|
bindCanvas();
|
||||||
|
bindMainEl();
|
||||||
|
bindChartClick();
|
||||||
setActiveTool("cursor");
|
setActiveTool("cursor");
|
||||||
if (chart && chart.timeScale) {
|
if (chart && chart.timeScale) {
|
||||||
if (unsubRange) {
|
if (unsubRange) {
|
||||||
@@ -1098,6 +1380,13 @@
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
unsubRange = null;
|
unsubRange = null;
|
||||||
}
|
}
|
||||||
|
if (unsubClick) {
|
||||||
|
try {
|
||||||
|
unsubClick();
|
||||||
|
} catch (_) {}
|
||||||
|
unsubClick = null;
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
setChartInteraction(true);
|
setChartInteraction(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v4" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260608-market-draw-v5" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="market-chart-body">
|
<div class="market-chart-body">
|
||||||
<aside id="market-draw-toolbar" class="market-draw-toolbar" aria-label="画线工具">
|
<aside id="market-draw-toolbar" class="market-draw-toolbar" aria-label="画线工具">
|
||||||
<button type="button" class="market-draw-btn is-active" data-tool="cursor" title="光标(拖动缩放图表)">
|
<button type="button" class="market-draw-btn is-active" data-tool="cursor" title="光标(点击选中,右键管理,Del 删除)">
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 4l7 16 2.5-5.5L20 12z"/></svg>
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M4 4l7 16 2.5-5.5L20 12z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="market-draw-btn" data-tool="hline" title="水平线">
|
<button type="button" class="market-draw-btn" data-tool="hline" title="水平线">
|
||||||
@@ -392,8 +392,8 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart_draw.js?v=20260608-market-draw-v4"></script>
|
<script src="/assets/chart_draw.js?v=20260608-market-draw-v5"></script>
|
||||||
<script src="/assets/chart.js?v=20260608-market-draw-v4"></script>
|
<script src="/assets/chart.js?v=20260608-market-draw-v5"></script>
|
||||||
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
|
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||||
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user