diff --git a/app.py b/app.py
index 64b2f2e..6e2f25c 100644
--- a/app.py
+++ b/app.py
@@ -30,6 +30,7 @@ from fee_specs import (
from fee_sync import sync_fees_from_akshare
from contract_profile import get_contract_profile
from stats_engine import STATS_VIEWS, load_stats_cache, refresh_stats_cache
+from kline_store import ensure_kline_tables
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
@@ -303,6 +304,7 @@ def init_db():
(key TEXT PRIMARY KEY,
data_json TEXT NOT NULL,
updated_at TEXT NOT NULL)''')
+ ensure_kline_tables(conn)
conn.commit()
conn.close()
@@ -1321,7 +1323,7 @@ def api_kline():
if not symbol:
return jsonify({"error": "请提供合约代码"}), 400
try:
- data = fetch_market_klines(symbol, period)
+ data = fetch_market_klines(symbol, period, DB_PATH)
except Exception as exc:
app.logger.warning("kline api failed: %s", exc)
return jsonify({"error": str(exc)}), 500
diff --git a/kline_chart.py b/kline_chart.py
index 21c411a..3fbfb2c 100644
--- a/kline_chart.py
+++ b/kline_chart.py
@@ -3,6 +3,7 @@ import json
import logging
import os
import re
+import sqlite3
from datetime import datetime
from typing import Optional
from zoneinfo import ZoneInfo
@@ -10,6 +11,7 @@ from zoneinfo import ZoneInfo
import requests
from symbols import ths_to_codes
+from kline_store import ensure_kline_tables, get_cached_entry, save_bars
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
@@ -213,14 +215,60 @@ def bars_to_api(bars: list) -> list[dict]:
return result
-def fetch_market_klines(symbol: str, period: str) -> dict:
+def fetch_market_klines(symbol: str, period: str, db_path: Optional[str] = None) -> dict:
chart_sym = ths_to_sina_chart_symbol(symbol)
p = (period or "15m").lower()
if p == "timeshare":
chart_type = "line"
else:
chart_type = "candle"
- bars = fetch_sina_klines(symbol, p)
+
+ bars: list = []
+ source = "remote"
+ cached_at = None
+
+ if db_path and chart_sym:
+ try:
+ conn = sqlite3.connect(db_path)
+ cached = get_cached_entry(conn, chart_sym, p)
+ conn.close()
+ if cached and cached.get("fresh"):
+ bars = cached["bars"]
+ source = "local"
+ cached_at = cached.get("updated_at")
+ except Exception as exc:
+ logger.warning("kline cache read failed %s %s: %s", chart_sym, p, exc)
+
+ if not bars:
+ remote_bars = fetch_sina_klines(symbol, p)
+ if remote_bars:
+ bars = remote_bars
+ source = "remote"
+ if db_path and chart_sym:
+ try:
+ conn = sqlite3.connect(db_path)
+ ensure_kline_tables(conn)
+ save_bars(conn, chart_sym, p, remote_bars)
+ meta = conn.execute(
+ "SELECT updated_at FROM kline_meta WHERE chart_symbol=? AND period=?",
+ (chart_sym, p),
+ ).fetchone()
+ conn.close()
+ cached_at = meta[0] if meta else None
+ except Exception as exc:
+ logger.warning("kline cache write failed %s %s: %s", chart_sym, p, exc)
+ elif db_path and chart_sym:
+ try:
+ conn = sqlite3.connect(db_path)
+ cached = get_cached_entry(conn, chart_sym, p)
+ conn.close()
+ if cached and cached.get("bars"):
+ bars = cached["bars"]
+ source = "local"
+ cached_at = cached.get("updated_at")
+ except Exception as exc:
+ logger.warning("kline cache fallback failed %s %s: %s", chart_sym, p, exc)
+
return {
"symbol": symbol,
"chart_symbol": chart_sym,
@@ -228,6 +276,8 @@ def fetch_market_klines(symbol: str, period: str) -> dict:
"chart_type": chart_type,
"count": len(bars),
"bars": bars_to_api(bars),
+ "source": source,
+ "cached_at": cached_at,
}
diff --git a/kline_store.py b/kline_store.py
new file mode 100644
index 0000000..3321c0f
--- /dev/null
+++ b/kline_store.py
@@ -0,0 +1,170 @@
+"""K 线本地 SQLite 缓存。"""
+from __future__ import annotations
+
+import sqlite3
+from datetime import datetime, timedelta
+from typing import Optional
+from zoneinfo import ZoneInfo
+
+TZ = ZoneInfo("Asia/Shanghai")
+
+REFRESH_SECONDS = {
+ "timeshare": 30,
+ "1m": 30,
+ "2m": 30,
+ "5m": 60,
+ "15m": 60,
+ "1h": 120,
+ "2h": 120,
+ "4h": 180,
+ "d": 300,
+ "w": 600,
+}
+
+
+def ensure_kline_tables(conn: sqlite3.Connection) -> None:
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS kline_bars (
+ chart_symbol TEXT NOT NULL,
+ period TEXT NOT NULL,
+ bar_time TEXT NOT NULL,
+ open REAL NOT NULL,
+ high REAL NOT NULL,
+ low REAL NOT NULL,
+ close REAL NOT NULL,
+ volume REAL DEFAULT 0,
+ updated_at TEXT NOT NULL,
+ PRIMARY KEY (chart_symbol, period, bar_time)
+ )"""
+ )
+ conn.execute(
+ """CREATE TABLE IF NOT EXISTS kline_meta (
+ chart_symbol TEXT NOT NULL,
+ period TEXT NOT NULL,
+ bar_count INTEGER DEFAULT 0,
+ last_bar_time TEXT,
+ updated_at TEXT NOT NULL,
+ PRIMARY KEY (chart_symbol, period)
+ )"""
+ )
+ conn.execute(
+ "CREATE INDEX IF NOT EXISTS idx_kline_bars_sym_period "
+ "ON kline_bars(chart_symbol, period, bar_time)"
+ )
+ conn.commit()
+
+
+def _parse_updated_at(value: str) -> Optional[datetime]:
+ if not value:
+ return None
+ try:
+ return datetime.fromisoformat(value.strip()).replace(tzinfo=TZ)
+ except ValueError:
+ return None
+
+
+def is_cache_fresh(period: str, updated_at: str) -> bool:
+ dt = _parse_updated_at(updated_at)
+ if not dt:
+ return False
+ ttl = REFRESH_SECONDS.get((period or "").lower(), 60)
+ return datetime.now(TZ) - dt < timedelta(seconds=ttl)
+
+
+def load_bars(conn: sqlite3.Connection, chart_symbol: str, period: str) -> list[dict]:
+ rows = conn.execute(
+ """SELECT bar_time, open, high, low, close, volume
+ FROM kline_bars
+ WHERE chart_symbol=? AND period=?
+ ORDER BY bar_time ASC""",
+ (chart_symbol, period),
+ ).fetchall()
+ return [
+ {
+ "d": row[0],
+ "o": float(row[1]),
+ "h": float(row[2]),
+ "l": float(row[3]),
+ "c": float(row[4]),
+ "v": float(row[5] or 0),
+ }
+ for row in rows
+ ]
+
+
+def load_meta(conn: sqlite3.Connection, chart_symbol: str, period: str) -> Optional[dict]:
+ row = conn.execute(
+ "SELECT bar_count, last_bar_time, updated_at FROM kline_meta "
+ "WHERE chart_symbol=? AND period=?",
+ (chart_symbol, period),
+ ).fetchone()
+ if not row:
+ return None
+ return {
+ "bar_count": row[0],
+ "last_bar_time": row[1],
+ "updated_at": row[2],
+ }
+
+
+def save_bars(conn: sqlite3.Connection, chart_symbol: str, period: str, bars: list[dict]) -> int:
+ if not bars:
+ return 0
+ ensure_kline_tables(conn)
+ now = datetime.now(TZ).isoformat(timespec="seconds")
+ for bar in bars:
+ conn.execute(
+ """INSERT INTO kline_bars
+ (chart_symbol, period, bar_time, open, high, low, close, volume, updated_at)
+ VALUES (?,?,?,?,?,?,?,?,?)
+ ON CONFLICT(chart_symbol, period, bar_time) DO UPDATE SET
+ open=excluded.open,
+ high=excluded.high,
+ low=excluded.low,
+ close=excluded.close,
+ volume=excluded.volume,
+ updated_at=excluded.updated_at""",
+ (
+ chart_symbol,
+ period,
+ str(bar["d"]),
+ float(bar["o"]),
+ float(bar["h"]),
+ float(bar["l"]),
+ float(bar["c"]),
+ float(bar.get("v") or 0),
+ now,
+ ),
+ )
+ last_time = str(bars[-1]["d"])
+ conn.execute(
+ """INSERT INTO kline_meta (chart_symbol, period, bar_count, last_bar_time, updated_at)
+ VALUES (?,?,?,?,?)
+ ON CONFLICT(chart_symbol, period) DO UPDATE SET
+ bar_count=excluded.bar_count,
+ last_bar_time=excluded.last_bar_time,
+ updated_at=excluded.updated_at""",
+ (chart_symbol, period, len(bars), last_time, now),
+ )
+ conn.commit()
+ return len(bars)
+
+
+def get_cached_entry(
+ conn: sqlite3.Connection,
+ chart_symbol: str,
+ period: str,
+) -> Optional[dict]:
+ if not chart_symbol:
+ return None
+ ensure_kline_tables(conn)
+ meta = load_meta(conn, chart_symbol, period)
+ bars = load_bars(conn, chart_symbol, period)
+ if not bars:
+ return None
+ updated_at = meta["updated_at"] if meta else ""
+ return {
+ "bars": bars,
+ "updated_at": updated_at,
+ "fresh": is_cache_fresh(period, updated_at),
+ }
diff --git a/static/js/market.js b/static/js/market.js
index f3b0022..ccc2bf1 100644
--- a/static/js/market.js
+++ b/static/js/market.js
@@ -5,6 +5,11 @@
var chart = null;
var currentPeriod = '15m';
var quoteTimer = null;
+ var klineTimer = null;
+ var lastData = null;
+ var klineLoading = false;
+
+ var FAST_PERIODS = ['timeshare', '1m', '2m', '5m', '15m', '1h', '2h', '4h'];
function getSymbol() {
var hidden = document.getElementById('market-symbol-hidden');
@@ -33,9 +38,40 @@
down: dark ? '#ff6b7a' : '#dc2626',
line: dark ? '#4cc2ff' : '#2563eb',
area: dark ? 'rgba(76,194,255,0.12)' : 'rgba(37,99,235,0.1)',
+ slider: dark ? '#1e2640' : '#cbd5e1',
+ sliderFill: dark ? '#4cc2ff' : '#2563eb',
};
}
+ function isTradingSession() {
+ var d = new Date();
+ var wd = d.getDay();
+ if (wd === 0) return false;
+ if (wd === 6 && d.getHours() < 21) return false;
+ var t = d.getHours() * 60 + d.getMinutes();
+ function inRange(sh, sm, eh, em) {
+ return t >= sh * 60 + sm && t < eh * 60 + em;
+ }
+ if (inRange(9, 0, 11, 30)) return true;
+ if (inRange(13, 30, 15, 0)) return true;
+ if (inRange(21, 0, 24, 0)) return true;
+ if (inRange(0, 0, 2, 30)) return true;
+ return false;
+ }
+
+ function klinePollMs() {
+ if (!isTradingSession()) return 0;
+ if (currentPeriod === 'timeshare' || FAST_PERIODS.indexOf(currentPeriod) >= 0) {
+ return 1000;
+ }
+ if (currentPeriod === 'd' || currentPeriod === 'w') return 30000;
+ return 5000;
+ }
+
+ function quotePollMs() {
+ return isTradingSession() ? 1000 : 10000;
+ }
+
function initChart() {
if (!chartEl || !window.echarts) return;
chart = echarts.init(chartEl);
@@ -45,42 +81,90 @@
document.addEventListener('click', function (e) {
if (e.target.closest('[data-theme-pick]')) {
setTimeout(function () {
- if (chart && lastData) renderChart(lastData);
+ if (chart && lastData) renderChart(lastData, true);
}, 100);
}
});
}
- var lastData = null;
+ function getDataZoom(c, preserve) {
+ var defStart = currentPeriod === 'timeshare' ? 60 : 75;
+ var zoom = [
+ {
+ type: 'inside',
+ xAxisIndex: [0, 1],
+ start: defStart,
+ end: 100,
+ zoomOnMouseWheel: true,
+ moveOnMouseMove: true,
+ moveOnMouseWheel: false,
+ },
+ {
+ type: 'slider',
+ xAxisIndex: [0, 1],
+ start: defStart,
+ end: 100,
+ height: 22,
+ bottom: 4,
+ borderColor: c.grid,
+ backgroundColor: c.bg,
+ fillerColor: c.area,
+ handleStyle: { color: c.sliderFill },
+ dataBackground: {
+ lineStyle: { color: c.grid },
+ areaStyle: { color: c.area },
+ },
+ textStyle: { color: c.text, fontSize: 10 },
+ },
+ ];
+ if (preserve && chart) {
+ var opt = chart.getOption();
+ if (opt && opt.dataZoom) {
+ opt.dataZoom.forEach(function (z, i) {
+ if (zoom[i] && z.start != null && z.end != null) {
+ zoom[i].start = z.start;
+ zoom[i].end = z.end;
+ }
+ });
+ }
+ }
+ return zoom;
+ }
- function renderChart(data) {
+ function renderChart(data, preserveZoom) {
if (!chart) return;
lastData = data;
var c = themeColors();
var bars = data.bars || [];
var times = bars.map(function (b) { return b.time; });
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
+ var dataZoom = getDataZoom(c, preserveZoom);
+ var grids = [
+ { left: 56, right: 20, top: 44, height: '50%' },
+ { left: 56, right: 20, top: '66%', height: '14%' },
+ ];
+
+ var base = {
+ backgroundColor: c.bg,
+ animation: false,
+ tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
+ axisPointer: { link: [{ xAxisIndex: 'all' }] },
+ dataZoom: dataZoom,
+ grid: grids,
+ xAxis: [
+ { type: 'category', data: times, gridIndex: 0, axisLabel: { color: c.text, fontSize: 10 }, axisLine: { lineStyle: { color: c.grid } } },
+ { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false }, axisLine: { lineStyle: { color: c.grid } } },
+ ],
+ yAxis: [
+ { scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } },
+ { scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 },
+ ],
+ };
if (isLine) {
var closes = bars.map(function (b) { return b.close; });
var vols = bars.map(function (b) { return b.volume; });
- chart.setOption({
- backgroundColor: c.bg,
- animation: false,
- tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
- axisPointer: { link: [{ xAxisIndex: 'all' }] },
- grid: [
- { left: 56, right: 16, top: 40, height: '58%' },
- { left: 56, right: 16, top: '72%', height: '18%' },
- ],
- xAxis: [
- { type: 'category', data: times, gridIndex: 0, axisLabel: { color: c.text, fontSize: 10 }, axisLine: { lineStyle: { color: c.grid } } },
- { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false }, axisLine: { lineStyle: { color: c.grid } } },
- ],
- yAxis: [
- { scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } },
- { scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 },
- ],
+ chart.setOption(Object.assign(base, {
series: [
{
name: '价格',
@@ -102,19 +186,17 @@
yAxisIndex: 1,
},
],
- }, true);
+ }), true);
} else {
var candle = bars.map(function (b) { return [b.open, b.close, b.low, b.high]; });
- var vols = bars.map(function (b, i) {
+ var vols = bars.map(function (b) {
var up = b.close >= b.open;
return {
value: b.volume,
itemStyle: { color: up ? c.up : c.down, opacity: 0.65 },
};
});
- chart.setOption({
- backgroundColor: c.bg,
- animation: false,
+ chart.setOption(Object.assign(base, {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
@@ -132,19 +214,6 @@
].join('
');
},
},
- axisPointer: { link: [{ xAxisIndex: 'all' }] },
- grid: [
- { left: 56, right: 16, top: 40, height: '58%' },
- { left: 56, right: 16, top: '72%', height: '18%' },
- ],
- xAxis: [
- { type: 'category', data: times, gridIndex: 0, axisLabel: { color: c.text, fontSize: 10 }, axisLine: { lineStyle: { color: c.grid } } },
- { type: 'category', data: times, gridIndex: 1, axisLabel: { show: false }, axisLine: { lineStyle: { color: c.grid } } },
- ],
- yAxis: [
- { scale: true, gridIndex: 0, splitLine: { lineStyle: { color: c.grid } }, axisLabel: { color: c.text } },
- { scale: true, gridIndex: 1, splitLine: { show: false }, axisLabel: { color: c.text, fontSize: 10 }, splitNumber: 2 },
- ],
series: [
{
name: 'K线',
@@ -167,11 +236,13 @@
yAxisIndex: 1,
},
],
- }, true);
+ }), true);
}
var title = (data.chart_symbol || data.symbol || '') + ' · ' + periodLabel(data.period);
- chart.setOption({ title: { text: title, left: 12, top: 8, textStyle: { color: c.title, fontSize: 13, fontWeight: 600 } } });
+ chart.setOption({
+ title: { text: title, left: 12, top: 8, textStyle: { color: c.title, fontSize: 13, fontWeight: 600 } },
+ });
}
function periodLabel(key) {
@@ -182,26 +253,61 @@
return key;
}
+ function hideEmptyOverlay() {
+ if (emptyEl) {
+ emptyEl.style.display = '';
+ }
+ if (wrapEl) wrapEl.classList.add('has-data');
+ }
+
+ function showEmptyOverlay(text) {
+ if (emptyEl) {
+ emptyEl.textContent = text;
+ emptyEl.style.display = 'flex';
+ }
+ if (wrapEl) wrapEl.classList.remove('has-data');
+ }
+
function setLoading(on) {
var btn = document.getElementById('market-load-btn');
if (btn) {
btn.disabled = on;
btn.textContent = on ? '加载中…' : '查看';
}
- if (emptyEl && on) {
- emptyEl.textContent = '加载中…';
- emptyEl.style.display = 'flex';
+ if (on) {
+ showEmptyOverlay('加载中…');
+ } else if (lastData) {
+ hideEmptyOverlay();
}
}
- function loadKline() {
- var symbol = getSymbol();
- if (!symbol) {
- alert('请先选择或输入合约代码');
+ function updateRefreshHint() {
+ var el = document.getElementById('market-refresh-hint');
+ if (!el) return;
+ if (!getSymbol()) {
+ el.textContent = '';
return;
}
- setLoading(true);
- if (wrapEl) wrapEl.classList.remove('has-data');
+ if (isTradingSession()) {
+ var ms = klinePollMs();
+ var src = lastData && lastData.source === 'local' ? ' · 本地缓存' : '';
+ el.textContent = ms === 1000
+ ? '交易中 · 1秒刷新' + src
+ : '交易中 · 自动刷新' + src;
+ } else {
+ el.textContent = '非交易时段 · 暂停高频刷新';
+ }
+ }
+
+ function loadKline(silent) {
+ var symbol = getSymbol();
+ if (!symbol) {
+ if (!silent) alert('请先选择或输入合约代码');
+ return;
+ }
+ if (klineLoading) return;
+ klineLoading = true;
+ if (!silent) setLoading(true);
var url = '/api/kline?symbol=' + encodeURIComponent(symbol) + '&period=' + encodeURIComponent(currentPeriod);
fetch(url)
@@ -209,22 +315,23 @@
return r.json().then(function (j) { return { ok: r.ok, data: j }; });
})
.then(function (res) {
- if (!res.ok) {
- throw new Error(res.data.error || '加载失败');
- }
- if (wrapEl) wrapEl.classList.add('has-data');
- renderChart(res.data);
+ if (!res.ok) throw new Error(res.data.error || '加载失败');
+ hideEmptyOverlay();
+ renderChart(res.data, silent);
updateQuoteMeta(res.data);
- startQuotePoll();
+ updateRefreshHint();
+ if (!quoteTimer) startQuotePoll();
+ if (!klineTimer) startKlinePoll();
})
.catch(function (err) {
- if (emptyEl) {
- emptyEl.textContent = err.message || '加载失败';
- emptyEl.style.display = 'flex';
+ if (!silent) {
+ showEmptyOverlay(err.message || '加载失败');
}
- if (wrapEl) wrapEl.classList.remove('has-data');
})
- .finally(function () { setLoading(false); });
+ .finally(function () {
+ klineLoading = false;
+ if (!silent) setLoading(false);
+ });
}
function updateQuoteMeta(data) {
@@ -261,7 +368,48 @@
function startQuotePoll() {
if (quoteTimer) clearInterval(quoteTimer);
loadQuote();
- quoteTimer = setInterval(loadQuote, 5000);
+ var ms = quotePollMs();
+ if (ms > 0) quoteTimer = setInterval(loadQuote, ms);
+ }
+
+ function startKlinePoll() {
+ if (klineTimer) clearInterval(klineTimer);
+ var ms = klinePollMs();
+ if (ms > 0 && getSymbol()) {
+ klineTimer = setInterval(function () {
+ loadKline(true);
+ updateRefreshHint();
+ }, ms);
+ }
+ }
+
+ function restartPollers() {
+ startQuotePoll();
+ startKlinePoll();
+ updateRefreshHint();
+ }
+
+ function shiftDataZoom(delta) {
+ if (!chart) return;
+ var opt = chart.getOption();
+ if (!opt || !opt.dataZoom || !opt.dataZoom.length) return;
+ var z = opt.dataZoom[0];
+ var span = (z.end - z.start) || 20;
+ var newSpan = Math.max(5, Math.min(100, span + delta));
+ var center = (z.start + z.end) / 2;
+ var start = Math.max(0, center - newSpan / 2);
+ var end = Math.min(100, center + newSpan / 2);
+ if (end - start < newSpan) {
+ if (start === 0) end = newSpan;
+ else start = end - newSpan;
+ }
+ chart.dispatchAction({ type: 'dataZoom', start: start, end: end });
+ }
+
+ function resetDataZoom() {
+ if (!chart) return;
+ var start = currentPeriod === 'timeshare' ? 60 : 75;
+ chart.dispatchAction({ type: 'dataZoom', start: start, end: 100 });
}
function bindPeriodTabs() {
@@ -273,25 +421,42 @@
tabs.querySelectorAll('.period-tab').forEach(function (el) { el.classList.remove('active'); });
btn.classList.add('active');
currentPeriod = btn.getAttribute('data-period') || '15m';
- if (getSymbol()) loadKline();
+ restartPollers();
+ if (getSymbol()) loadKline(false);
});
}
+ function bindZoomButtons() {
+ var zoomIn = document.getElementById('chart-zoom-in');
+ var zoomOut = document.getElementById('chart-zoom-out');
+ var zoomReset = document.getElementById('chart-zoom-reset');
+ if (zoomIn) zoomIn.addEventListener('click', function () { shiftDataZoom(-12); });
+ if (zoomOut) zoomOut.addEventListener('click', function () { shiftDataZoom(12); });
+ if (zoomReset) zoomReset.addEventListener('click', resetDataZoom);
+ }
+
document.addEventListener('DOMContentLoaded', function () {
initChart();
bindPeriodTabs();
+ bindZoomButtons();
var active = document.querySelector('.period-tab.active');
if (active) currentPeriod = active.getAttribute('data-period') || '15m';
var loadBtn = document.getElementById('market-load-btn');
- if (loadBtn) loadBtn.addEventListener('click', loadKline);
+ if (loadBtn) loadBtn.addEventListener('click', function () {
+ restartPollers();
+ loadKline(false);
+ });
var hidden = document.getElementById('market-symbol-hidden');
var input = document.getElementById('market-symbol-input');
if (hidden && hidden.value) {
if (input && !input.value) input.value = hidden.value;
- loadKline();
+ restartPollers();
+ loadKline(false);
+ } else {
+ updateRefreshHint();
}
});
})();
diff --git a/templates/base.html b/templates/base.html
index 0745614..498e86c 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -388,21 +388,38 @@
.pos-del{font-size:.75rem;padding:.35rem .65rem}
.trade-toolbar{display:flex;align-items:center;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
.trade-switch-label{
- display:flex;align-items:center;gap:.35rem;
- font-size:.68rem;color:var(--text-muted);
- white-space:nowrap;margin-bottom:.65rem;cursor:pointer;
+ display:flex;align-items:center;gap:.4rem;
+ font-size:.78rem;color:var(--text-muted);
+ white-space:normal;margin-bottom:.65rem;cursor:pointer;
+ line-height:1.45;max-width:100%;
}
- .trade-switch-label span{line-height:1}
- .trade-switch-label input{flex-shrink:0}
- .trade-table-wrap{overflow-x:auto}
- .trade-table{font-size:.8rem}
- .trade-table th{font-size:.75rem;padding:.55rem .45rem}
- .trade-table td{padding:.45rem .4rem;vertical-align:middle}
+ .trade-switch-label span{line-height:1.45;color:var(--text-muted)}
+ .trade-switch-label input{flex-shrink:0;width:auto}
+ .trade-table-wrap{
+ overflow:auto;
+ max-height:420px;
+ width:100%;
+ -webkit-overflow-scrolling:touch;
+ border-radius:10px;
+ border:1px solid var(--table-border);
+ background:var(--card-inner);
+ }
+ .trade-table{font-size:.8rem;width:max-content;min-width:100%;table-layout:auto}
+ .trade-table th{font-size:.75rem;padding:.55rem .45rem;white-space:nowrap;background:var(--card-inner)}
+ .trade-table td{padding:.45rem .4rem;vertical-align:middle;white-space:nowrap;background:var(--card-inner)}
+ .trade-table th:last-child,
+ .trade-table td:last-child{
+ position:sticky;right:0;z-index:3;
+ box-shadow:-6px 0 10px rgba(0,0,0,.08);
+ }
+ .trade-table thead th:last-child{z-index:4}
.trade-table input,.trade-table select{
padding:.35rem .45rem;font-size:.78rem;border-radius:6px;width:100%;min-width:0;
}
.trade-table .cell-readonly{color:var(--text-primary)}
- .trade-actions{display:flex;gap:.35rem;flex-wrap:wrap}
+ .records-trade-card{overflow:visible}
+ .records-trade-card .card-body{overflow:visible}
+ .trade-actions{display:flex;gap:.35rem;flex-wrap:wrap;align-items:center;min-width:148px}
.trade-actions a,.trade-actions button{font-size:.72rem;padding:.3rem .55rem;border-radius:6px;text-decoration:none;border:none;cursor:pointer}
.btn-fill{background:var(--dir-bg);color:var(--accent)}
.btn-verify{background:var(--nav-active);color:#fff}
diff --git a/templates/market.html b/templates/market.html
index 064279e..4ede7ef 100644
--- a/templates/market.html
+++ b/templates/market.html
@@ -26,11 +26,19 @@
—
+
数据来源:新浪财经。分时图为当日分钟走势;2分/2小时/周线由基础周期合成。
+数据来源:新浪财经。支持滚轮/拖拽缩放 K 线;交易时段内行情与 K 线约 1 秒刷新。