dsh-xueqiu 1.7.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +7 -0
- package/dynamic/client.js +159 -22
- package/package.json +1 -1
- package/src/client/index.js +159 -22
package/README.en.md
CHANGED
|
@@ -121,6 +121,8 @@ The data layer is defended in depth — safe to leave open for hours:
|
|
|
121
121
|
|
|
122
122
|
## Changelog
|
|
123
123
|
|
|
124
|
+
- **1.9.0** (2025-08-19) — Micro-interactions (tab fade, refresh spinner), news timeline with day-group anchors, badge hover watchlist preview, and crosshair axis labels (price + date/time) on K-line/minute charts.
|
|
125
|
+
- **1.8.0** (2025-08-19) — Pro polish: tabular-nums everywhere (no column jitter), red/green percent chips, and price-change flash animation on watchlist rows.
|
|
124
126
|
- **1.7.2** (2025-08-19) — News tab pagination: scroll-to-bottom or "load earlier" fetches older items (max_id cursor, deduped); refresh resets to the latest page.
|
|
125
127
|
- **1.7.1** (2025-08-19) — Fix static install (`dsh plugin add`) crashing `dsh web` startup: dual-mode RPC (harness.handle for dynamic runs, a loopback+same-origin fenced `/xq-rpc` webServer route for static installs) and a proper `__ModuleLoader__` CJS client bundle.
|
|
126
128
|
- **1.7.0** (2025-08-19) — Hidden-tab polling pause + CN/HK/US market session hints.
|
package/README.md
CHANGED
|
@@ -151,6 +151,13 @@ dsh-xueqiu/
|
|
|
151
151
|
|
|
152
152
|
## 📋 更新日志
|
|
153
153
|
|
|
154
|
+
- **1.9.0**(2025-08-19)
|
|
155
|
+
- 微交互:tab/详情切换淡入动画、刷新按钮旋转 loading 态。
|
|
156
|
+
- 快讯时间轴:左侧竖线 + 「今天/昨天/X月X日」分组锚点,长列表快速定位时间。
|
|
157
|
+
- 徽章 hover 预览:悬停 300ms 弹出自选速览(名称/现价/涨跌幅 chip,30s 缓存),点击展开面板,拖拽不误触。
|
|
158
|
+
- K线/分时十字光标轴标签:光标价位(右侧)+ 日期/时间(底部)底色读数块。
|
|
159
|
+
- **1.8.0**(2025-08-19)
|
|
160
|
+
- 专业感升级三件套:全界面等宽数字(tabular-nums,价格跳动不抖列);涨跌幅色块 chip(红涨/绿跌半透明底,扫一眼定位);自选行价格变动闪烁动画(涨红闪/跌绿闪,0.8s 渐隐)。
|
|
154
161
|
- **1.7.2**(2025-08-19)
|
|
155
162
|
- 新增:快讯 tab 翻页——滚动到底或点「加载更早」自动加载历史快讯(max_id 游标,自动去重),「刷新」重置回最新一页。
|
|
156
163
|
- **1.7.1**(2025-08-19)
|
package/dynamic/client.js
CHANGED
|
@@ -41,6 +41,36 @@ return {
|
|
|
41
41
|
'.xq-up{color:var(--dsw-alias-state-error-primary);}\n' +
|
|
42
42
|
'.xq-down{color:var(--dsw-alias-state-success-primary);}\n' +
|
|
43
43
|
'.xq-flat{color:var(--dsw-alias-label-secondary);}\n' +
|
|
44
|
+
// 专业感三件套:①等宽数字 ②涨跌幅色块 ③价格闪烁
|
|
45
|
+
'.xq-dock,.xq-badge,.xq-ticker{font-variant-numeric:tabular-nums;}\n' +
|
|
46
|
+
'.xq-pct-chip{display:inline-block;min-width:52px;text-align:center;padding:1px 6px;border-radius:5px;font-size:11.5px;font-weight:600;}\n' +
|
|
47
|
+
'.xq-pct-chip.xq-up{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);}\n' +
|
|
48
|
+
'.xq-pct-chip.xq-down{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);}\n' +
|
|
49
|
+
'.xq-pct-chip.xq-flat{background:var(--dsw-alias-bg-layer-2);}\n' +
|
|
50
|
+
'.xq-idx-pct.xq-pct-chip{min-width:46px;padding:0 5px;font-size:10.5px;}\n' +
|
|
51
|
+
'@keyframes xq-flash-up{0%{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 18%, transparent);}100%{background:transparent;}}\n' +
|
|
52
|
+
'@keyframes xq-flash-down{0%{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 18%, transparent);}100%{background:transparent;}}\n' +
|
|
53
|
+
'.xq-flash-up{animation:xq-flash-up .8s ease-out;}\n' +
|
|
54
|
+
'.xq-flash-down{animation:xq-flash-down .8s ease-out;}\n' +
|
|
55
|
+
// 微交互:内容切换淡入 + 刷新按钮旋转
|
|
56
|
+
'@keyframes xq-fade-in{from{opacity:0;transform:translateY(3px);}to{opacity:1;transform:none;}}\n' +
|
|
57
|
+
'.xq-view{animation:xq-fade-in .18s ease-out;}\n' +
|
|
58
|
+
'@keyframes xq-spin{to{transform:rotate(360deg);}}\n' +
|
|
59
|
+
'.xq-refresh-spin{display:inline-block;animation:xq-spin .8s linear infinite;}\n' +
|
|
60
|
+
'.xq-tab{transition:color .15s,border-color .15s;}\n' +
|
|
61
|
+
// 快讯时间轴:左侧竖线 + 时间锚点分组
|
|
62
|
+
'.xq-news-tl{position:relative;padding-left:14px;}\n' +
|
|
63
|
+
'.xq-news-tl::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;width:1px;background:var(--dsw-alias-border-l2);}\n' +
|
|
64
|
+
'.xq-news-group{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin:8px 0 5px;display:flex;align-items:center;gap:6px;}\n' +
|
|
65
|
+
'.xq-news-group::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l2);margin-left:-13px;flex:none;}\n' +
|
|
66
|
+
'.xq-news-item{position:relative;}\n' +
|
|
67
|
+
// 徽章 hover 预览弹层
|
|
68
|
+
'.xq-badge-pop{position:fixed;z-index:1201;min-width:210px;max-width:280px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.28);padding:8px 10px;font-size:12px;}\n' +
|
|
69
|
+
'.xq-badge-pop-hd{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin-bottom:5px;display:flex;justify-content:space-between;}\n' +
|
|
70
|
+
'.xq-bp-row{display:flex;align-items:center;gap:8px;padding:2.5px 0;cursor:pointer;border-radius:5px;}\n' +
|
|
71
|
+
'.xq-bp-row:hover{background:var(--dsw-alias-bg-layer-2);}\n' +
|
|
72
|
+
'.xq-bp-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n' +
|
|
73
|
+
'.xq-bp-val{font-weight:600;}\n' +
|
|
44
74
|
'.xq-grid{display:grid;gap:0;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;margin-bottom:8px;}\n' +
|
|
45
75
|
'.xq-grid-hd,.xq-grid-row{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr 84px;align-items:center;}\n' +
|
|
46
76
|
'.xq-grid-hd{font-size:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);padding:5px 8px;}\n' +
|
|
@@ -244,8 +274,8 @@ return {
|
|
|
244
274
|
const rows = props.rows || []
|
|
245
275
|
const [hi, setHi] = React.useState(null)
|
|
246
276
|
if (!rows.length) return el('div', { className: 'xq-muted' }, '暂无K线数据')
|
|
247
|
-
const W = 640, MAIN = 150, VOL = 44, PAD = 6, GAP = 4
|
|
248
|
-
const H = MAIN + VOL + GAP + PAD
|
|
277
|
+
const W = 640, MAIN = 150, VOL = 44, PAD = 6, GAP = 4, LBL = 14
|
|
278
|
+
const H = MAIN + VOL + GAP + PAD + LBL
|
|
249
279
|
let min = Infinity, max = -Infinity
|
|
250
280
|
for (let i = 0; i < rows.length; i++) {
|
|
251
281
|
const rlo = Number(rows[i].low), rhi = Number(rows[i].high)
|
|
@@ -259,7 +289,7 @@ return {
|
|
|
259
289
|
const bw = Math.max(step * 0.66, 1)
|
|
260
290
|
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (MAIN - PAD * 2) }
|
|
261
291
|
const vTop = PAD + MAIN + GAP
|
|
262
|
-
const vBottom = H - PAD
|
|
292
|
+
const vBottom = H - PAD - LBL
|
|
263
293
|
const vH = vBottom - vTop
|
|
264
294
|
let vmax = 0
|
|
265
295
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -319,9 +349,18 @@ return {
|
|
|
319
349
|
const r = rows[hi]
|
|
320
350
|
const cx = PAD + step * hi + step / 2
|
|
321
351
|
const cy = yOf(Number(r.close))
|
|
352
|
+
// 轴标签:光标价位(右侧底色块)+ 日期(底部底色块)
|
|
353
|
+
const priceTxt = fmt(Number(r.close))
|
|
354
|
+
const pLblW = priceTxt.length * 5.6 + 8
|
|
355
|
+
const dateTxt = fmtFullDay(r.timestamp).slice(5) // MM-DD
|
|
356
|
+
const dLblW = dateTxt.length * 5.6 + 8
|
|
322
357
|
cross = [
|
|
323
358
|
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2: vBottom, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
324
|
-
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 })
|
|
359
|
+
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
360
|
+
el('rect', { key: 'pbg', x: W - PAD - pLblW, y: cy - 7, width: pLblW, height: 14, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
361
|
+
el('text', { key: 'ptx', x: W - PAD - pLblW / 2, y: cy + 3.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, priceTxt),
|
|
362
|
+
el('rect', { key: 'dbg', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW), y: vBottom + 1, width: dLblW, height: 13, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
363
|
+
el('text', { key: 'dtx', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW) + dLblW / 2, y: vBottom + 10.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, dateTxt)
|
|
325
364
|
]
|
|
326
365
|
tip = el('div', {
|
|
327
366
|
key: 'tip', className: 'xq-tip',
|
|
@@ -379,7 +418,7 @@ return {
|
|
|
379
418
|
const [hi, setHi] = React.useState(null)
|
|
380
419
|
const lastClose = Number(props.lastClose)
|
|
381
420
|
if (!items.length) return el('div', { className: 'xq-muted' }, '暂无分时数据')
|
|
382
|
-
const W = 640,
|
|
421
|
+
const W = 640, CH = 170, LBL = 14, H = CH + LBL, PAD = 6 // CH 图区 + LBL 底部时间标签条
|
|
383
422
|
let min = Infinity, max = -Infinity
|
|
384
423
|
for (let i = 0; i < items.length; i++) {
|
|
385
424
|
const c = Number(items[i].current)
|
|
@@ -389,7 +428,7 @@ return {
|
|
|
389
428
|
if (lastClose) { if (lastClose < min) min = lastClose; if (lastClose > max) max = lastClose }
|
|
390
429
|
if (!isFinite(min) || !isFinite(max) || min === max) { min -= 1; max += 1 }
|
|
391
430
|
const range = max - min
|
|
392
|
-
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (
|
|
431
|
+
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (CH - PAD * 2) }
|
|
393
432
|
const xOf = function (i) { return PAD + (i / (items.length - 1)) * (W - PAD * 2) }
|
|
394
433
|
const pricePts = []
|
|
395
434
|
const avgPts = []
|
|
@@ -417,9 +456,18 @@ return {
|
|
|
417
456
|
const it = items[hi]
|
|
418
457
|
const cx = xOf(hi), cy = yOf(Number(it.current))
|
|
419
458
|
const pct = lastClose ? (Number(it.current) - lastClose) / lastClose * 100 : null
|
|
459
|
+
// 轴标签:光标价位(右侧)+ 时间(底部)
|
|
460
|
+
const priceTxt = fmt(Number(it.current))
|
|
461
|
+
const pLblW = priceTxt.length * 5.6 + 8
|
|
462
|
+
const timeTxt = it.timestamp ? fmtTime(it.timestamp) : ''
|
|
463
|
+
const dLblW = timeTxt.length * 5.6 + 8
|
|
420
464
|
cross = [
|
|
421
|
-
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2:
|
|
422
|
-
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 })
|
|
465
|
+
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2: CH - PAD, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
466
|
+
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
467
|
+
el('rect', { key: 'pbg', x: W - PAD - pLblW, y: cy - 7, width: pLblW, height: 14, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
468
|
+
el('text', { key: 'ptx', x: W - PAD - pLblW / 2, y: cy + 3.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, priceTxt),
|
|
469
|
+
el('rect', { key: 'dbg', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW), y: CH + 1, width: dLblW, height: 13, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
470
|
+
el('text', { key: 'dtx', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW) + dLblW / 2, y: CH + 10.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, timeTxt)
|
|
423
471
|
]
|
|
424
472
|
tip = el('div', {
|
|
425
473
|
key: 'tip', className: 'xq-tip',
|
|
@@ -459,6 +507,9 @@ return {
|
|
|
459
507
|
function XueqiuPanel() {
|
|
460
508
|
const [watchlist, setWatchlist] = React.useState([])
|
|
461
509
|
const [quotes, setQuotes] = React.useState([])
|
|
510
|
+
// 价格闪烁:记录上一价与变化方向(symbol -> 1|-1),行渲染时挂一次性动画类
|
|
511
|
+
const prevPrices = React.useRef({})
|
|
512
|
+
const [flashDir, setFlashDir] = React.useState({})
|
|
462
513
|
const [indices, setIndices] = React.useState([])
|
|
463
514
|
const [hot, setHot] = React.useState([])
|
|
464
515
|
const [hotMarket, setHotMarket] = React.useState('cn')
|
|
@@ -495,7 +546,15 @@ return {
|
|
|
495
546
|
setErr('')
|
|
496
547
|
if (!wl.length) return
|
|
497
548
|
return call('quote', { symbols: wl }).then(function (data) {
|
|
498
|
-
|
|
549
|
+
const list = (data && data.list) || []
|
|
550
|
+
const dirs = {}
|
|
551
|
+
list.forEach(function (q) {
|
|
552
|
+
const prev = prevPrices.current[q.symbol]
|
|
553
|
+
if (prev !== undefined && q.current !== prev) dirs[q.symbol] = q.current > prev ? 1 : -1
|
|
554
|
+
prevPrices.current[q.symbol] = q.current
|
|
555
|
+
})
|
|
556
|
+
if (Object.keys(dirs).length) setFlashDir(dirs)
|
|
557
|
+
setQuotes(list)
|
|
499
558
|
})
|
|
500
559
|
}).catch(function (e) {
|
|
501
560
|
setErr(String((e && e.message) || e))
|
|
@@ -656,12 +715,12 @@ return {
|
|
|
656
715
|
}).concat([
|
|
657
716
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
658
717
|
el('button', {
|
|
659
|
-
key: 'btn', className: 'xq-btn', disabled: loading,
|
|
718
|
+
key: 'btn', className: 'xq-btn', disabled: loading, title: '刷新全部数据',
|
|
660
719
|
onClick: function () {
|
|
661
720
|
setLoading(true)
|
|
662
721
|
Promise.all([refreshMarket(), refreshContent()]).then(function () { setLoading(false) }).catch(function () { setLoading(false) })
|
|
663
722
|
}
|
|
664
|
-
}, '刷新')
|
|
723
|
+
}, loading ? [el('span', { key: 's', className: 'xq-refresh-spin' }, '⟳'), ' 刷新中'] : '刷新')
|
|
665
724
|
]))
|
|
666
725
|
|
|
667
726
|
const errBox = err ? el('div', { className: 'xq-err' }, err) : null
|
|
@@ -689,7 +748,7 @@ return {
|
|
|
689
748
|
el('div', { key: 'n', className: 'xq-idx-name' }, q.name),
|
|
690
749
|
el('div', { key: 'r', className: 'xq-idx-row' }, [
|
|
691
750
|
el('span', { key: 'c', className: 'xq-idx-cur ' + colorOf(q.percent) }, fmt(q.current)),
|
|
692
|
-
el('span', { key: 'p', className: 'xq-idx-pct ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
751
|
+
el('span', { key: 'p', className: 'xq-idx-pct xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
693
752
|
])
|
|
694
753
|
])
|
|
695
754
|
})
|
|
@@ -699,8 +758,8 @@ return {
|
|
|
699
758
|
el('div', { key: 'a', className: 'xq-name' }, q.name),
|
|
700
759
|
el('div', { key: 'b', className: 'xq-sub' }, q.symbol)
|
|
701
760
|
]),
|
|
702
|
-
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
703
|
-
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
761
|
+
el('div', { key: 'c', className: colorOf(q.percent) + (flashDir[q.symbol] === 1 ? ' xq-flash-up' : flashDir[q.symbol] === -1 ? ' xq-flash-down' : '') }, fmt(q.current)),
|
|
762
|
+
el('div', { key: 'p' }, el('span', { key: 'pp', className: 'xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))),
|
|
704
763
|
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
705
764
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
706
765
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); openDetail(q.symbol) } }, '详情'),
|
|
@@ -886,11 +945,25 @@ return {
|
|
|
886
945
|
}
|
|
887
946
|
|
|
888
947
|
function NewsTab() {
|
|
889
|
-
|
|
890
|
-
|
|
948
|
+
// 时间轴:按「今天/昨天/M月D日」分组,每组一个锚点头
|
|
949
|
+
const kids = []
|
|
950
|
+
let lastDay = null
|
|
951
|
+
news.forEach(function (it) {
|
|
952
|
+
const d = new Date(it.created_at)
|
|
953
|
+
const now = new Date()
|
|
954
|
+
const sameDay = function (a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() }
|
|
955
|
+
const yest = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
|
|
956
|
+
let label = null
|
|
957
|
+
const dayKey = d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate()
|
|
958
|
+
if (dayKey !== lastDay) {
|
|
959
|
+
lastDay = dayKey
|
|
960
|
+
label = sameDay(d, now) ? '今天' : (sameDay(d, yest) ? '昨天' : (d.getMonth() + 1) + '月' + d.getDate() + '日')
|
|
961
|
+
}
|
|
962
|
+
if (label) kids.push(el('div', { key: 'g' + dayKey, className: 'xq-news-group' }, label))
|
|
963
|
+
kids.push(el('div', { key: String(it.id), className: 'xq-news-item' + (it.mark === 1 ? ' xq-important' : '') }, [
|
|
891
964
|
el('div', { key: 't' }, it.text),
|
|
892
965
|
el('div', { key: 'm', className: 'xq-news-time' }, fmtTime(it.created_at) + (it.mark === 1 ? ' · 重要' : ''))
|
|
893
|
-
])
|
|
966
|
+
]))
|
|
894
967
|
})
|
|
895
968
|
// 滚动到底自动加载;手动入口兜底(滚轮惯性/触控板场景)
|
|
896
969
|
kids.push(el('div', {
|
|
@@ -900,7 +973,7 @@ return {
|
|
|
900
973
|
}, newsMore ? '加载中…' : (newsNoMore ? '没有更早的快讯了' : '加载更早 ↑')))
|
|
901
974
|
return news.length
|
|
902
975
|
? el('div', {
|
|
903
|
-
className: 'xq-news',
|
|
976
|
+
className: 'xq-news xq-news-tl',
|
|
904
977
|
onScroll: function (e) {
|
|
905
978
|
const n = e.target
|
|
906
979
|
if (n.scrollTop + n.clientHeight >= n.scrollHeight - 40) loadMoreNews()
|
|
@@ -922,7 +995,8 @@ return {
|
|
|
922
995
|
content = NewsTab()
|
|
923
996
|
}
|
|
924
997
|
|
|
925
|
-
|
|
998
|
+
// key 变化触发 xq-fade-in 重放:tab/详情切换有轻微淡入
|
|
999
|
+
return el('div', null, [tabs, errBox, el('div', { key: 'v-' + (view ? 'd' : tab), className: 'xq-view' }, content)])
|
|
926
1000
|
}
|
|
927
1001
|
|
|
928
1002
|
// ---------- 嵌入式主面板(conversation.input.dock):输入框上方一整行 ----------
|
|
@@ -1028,6 +1102,41 @@ return {
|
|
|
1028
1102
|
const [idx, setIdx] = React.useState([])
|
|
1029
1103
|
const [mOpen, setMOpen] = React.useState(true)
|
|
1030
1104
|
const [, force] = React.useState(0)
|
|
1105
|
+
// hover 预览:延迟出现,拖拽/点击不误触;数据 30s 内复用
|
|
1106
|
+
const [pop, setPop] = React.useState(null) // { quotes: [], at: ts }
|
|
1107
|
+
const hoverTimer = React.useRef(null)
|
|
1108
|
+
const popCache = React.useRef({ at: 0, list: [] })
|
|
1109
|
+
|
|
1110
|
+
function loadPop() {
|
|
1111
|
+
const now = Date.now()
|
|
1112
|
+
if (now - popCache.current.at < 30000 && popCache.current.list.length) {
|
|
1113
|
+
setPop({ quotes: popCache.current.list, at: popCache.current.at })
|
|
1114
|
+
return
|
|
1115
|
+
}
|
|
1116
|
+
// 两次 RPC(watchlist + quotes),走既有节流管线
|
|
1117
|
+
call('watchlist.get', {}).then(function (wl) {
|
|
1118
|
+
const symbols = (wl && wl.symbols) || []
|
|
1119
|
+
if (!symbols.length) { setPop({ quotes: [], at: now }); return }
|
|
1120
|
+
return call('quote', { symbols: symbols }).then(function (data) {
|
|
1121
|
+
const list = (data && data.list) || []
|
|
1122
|
+
popCache.current = { at: now, list: list }
|
|
1123
|
+
setPop({ quotes: list, at: now })
|
|
1124
|
+
})
|
|
1125
|
+
}).catch(function () { /* 静默 */ })
|
|
1126
|
+
}
|
|
1127
|
+
function onEnter() {
|
|
1128
|
+
if (hoverTimer.current) return
|
|
1129
|
+
hoverTimer.current = setTimeout(function () {
|
|
1130
|
+
hoverTimer.current = null
|
|
1131
|
+
if (!badgeDrag) loadPop()
|
|
1132
|
+
}, 300)
|
|
1133
|
+
}
|
|
1134
|
+
function onLeave() {
|
|
1135
|
+
if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null }
|
|
1136
|
+
setPop(null)
|
|
1137
|
+
}
|
|
1138
|
+
React.useEffect(function () { return function () { if (hoverTimer.current) clearTimeout(hoverTimer.current) } }, [])
|
|
1139
|
+
|
|
1031
1140
|
React.useEffect(function () {
|
|
1032
1141
|
let alive = true
|
|
1033
1142
|
function refresh() {
|
|
@@ -1091,11 +1200,37 @@ return {
|
|
|
1091
1200
|
const style = ui.badgePos
|
|
1092
1201
|
? { left: ui.badgePos.x, top: ui.badgePos.y, right: 'auto', bottom: 'auto' }
|
|
1093
1202
|
: null
|
|
1094
|
-
|
|
1203
|
+
// hover 预览弹层:跟随徽章位置(有拖拽坐标用之,否则用默认右下角),显示在徽章上方
|
|
1204
|
+
let popStyle = null
|
|
1205
|
+
if (ui.badgePos) {
|
|
1206
|
+
popStyle = { left: Math.max(6, ui.badgePos.x - 60), top: Math.max(6, ui.badgePos.y - 210), right: 'auto', bottom: 'auto' }
|
|
1207
|
+
} else {
|
|
1208
|
+
popStyle = { right: 16, bottom: 104, left: 'auto', top: 'auto' }
|
|
1209
|
+
}
|
|
1210
|
+
let popEl = null
|
|
1211
|
+
if (pop) {
|
|
1212
|
+
const rows = pop.quotes.slice(0, 8).map(function (q) {
|
|
1213
|
+
return el('div', {
|
|
1214
|
+
key: q.symbol, className: 'xq-bp-row', title: '点击查看详情',
|
|
1215
|
+
onClick: function (e) { e.stopPropagation(); setPop(null); ui.set({ open: true }) }
|
|
1216
|
+
}, [
|
|
1217
|
+
el('span', { key: 'n', className: 'xq-bp-name' }, q.name || q.symbol),
|
|
1218
|
+
el('span', { key: 'v', className: 'xq-bp-val ' + colorOf(q.percent) }, fmt(q.current)),
|
|
1219
|
+
el('span', { key: 'p', className: 'xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
1220
|
+
])
|
|
1221
|
+
})
|
|
1222
|
+
popEl = el('div', { key: 'pop', className: 'xq-badge-pop', style: popStyle, onMouseEnter: onEnter, onMouseLeave: onLeave }, [
|
|
1223
|
+
el('div', { key: 'hd', className: 'xq-badge-pop-hd' }, [el('span', { key: 't' }, '自选 · ' + pop.quotes.length + ' 只'), el('span', { key: 'h' }, '悬停查看 · 点击展开')]),
|
|
1224
|
+
rows.length ? rows : el('div', { key: 'e', className: 'xq-muted' }, '自选为空')
|
|
1225
|
+
])
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const badgeEl = el('div', {
|
|
1095
1229
|
className: 'xq-badge',
|
|
1096
1230
|
style: style,
|
|
1097
|
-
title: '点击开合行情面板 · 拖动调整位置',
|
|
1098
|
-
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp
|
|
1231
|
+
title: '点击开合行情面板 · 拖动调整位置 · 悬停速览自选',
|
|
1232
|
+
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp,
|
|
1233
|
+
onMouseEnter: onEnter, onMouseLeave: onLeave
|
|
1099
1234
|
}, [
|
|
1100
1235
|
el('span', { key: 'logo' }, [el('b', { key: 'b' }, '雪球'), 'mini']),
|
|
1101
1236
|
el('span', { key: 'st', className: 'xq-status' + (aSession() === '盘中' ? ' xq-live' : ''), title: sessionsText() }, (function () { var s = aSession(); return s === '盘中' ? '● 盘中' : (s || (mOpen ? '● 盘中' : '休市')) })()),
|
|
@@ -1110,6 +1245,8 @@ return {
|
|
|
1110
1245
|
]) : null,
|
|
1111
1246
|
el('span', { key: 'ct', className: 'xq-caret' }, ui.open ? '▾' : '▴')
|
|
1112
1247
|
])
|
|
1248
|
+
// 徽章 + 弹层同层渲染(都为 fixed 定位,互不嵌套,拖拽事件不穿透到弹层)
|
|
1249
|
+
return el('div', { style: { contents: 'display' } }, popEl ? [badgeEl, popEl] : [badgeEl])
|
|
1113
1250
|
}
|
|
1114
1251
|
|
|
1115
1252
|
// ---------- 底部指数条(会话页,输入框下方氛围行) ----------
|
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -60,6 +60,36 @@ exports.default = {
|
|
|
60
60
|
'.xq-up{color:var(--dsw-alias-state-error-primary);}\n' +
|
|
61
61
|
'.xq-down{color:var(--dsw-alias-state-success-primary);}\n' +
|
|
62
62
|
'.xq-flat{color:var(--dsw-alias-label-secondary);}\n' +
|
|
63
|
+
// 专业感三件套:①等宽数字 ②涨跌幅色块 ③价格闪烁
|
|
64
|
+
'.xq-dock,.xq-badge,.xq-ticker{font-variant-numeric:tabular-nums;}\n' +
|
|
65
|
+
'.xq-pct-chip{display:inline-block;min-width:52px;text-align:center;padding:1px 6px;border-radius:5px;font-size:11.5px;font-weight:600;}\n' +
|
|
66
|
+
'.xq-pct-chip.xq-up{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);}\n' +
|
|
67
|
+
'.xq-pct-chip.xq-down{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);}\n' +
|
|
68
|
+
'.xq-pct-chip.xq-flat{background:var(--dsw-alias-bg-layer-2);}\n' +
|
|
69
|
+
'.xq-idx-pct.xq-pct-chip{min-width:46px;padding:0 5px;font-size:10.5px;}\n' +
|
|
70
|
+
'@keyframes xq-flash-up{0%{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 18%, transparent);}100%{background:transparent;}}\n' +
|
|
71
|
+
'@keyframes xq-flash-down{0%{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 18%, transparent);}100%{background:transparent;}}\n' +
|
|
72
|
+
'.xq-flash-up{animation:xq-flash-up .8s ease-out;}\n' +
|
|
73
|
+
'.xq-flash-down{animation:xq-flash-down .8s ease-out;}\n' +
|
|
74
|
+
// 微交互:内容切换淡入 + 刷新按钮旋转
|
|
75
|
+
'@keyframes xq-fade-in{from{opacity:0;transform:translateY(3px);}to{opacity:1;transform:none;}}\n' +
|
|
76
|
+
'.xq-view{animation:xq-fade-in .18s ease-out;}\n' +
|
|
77
|
+
'@keyframes xq-spin{to{transform:rotate(360deg);}}\n' +
|
|
78
|
+
'.xq-refresh-spin{display:inline-block;animation:xq-spin .8s linear infinite;}\n' +
|
|
79
|
+
'.xq-tab{transition:color .15s,border-color .15s;}\n' +
|
|
80
|
+
// 快讯时间轴:左侧竖线 + 时间锚点分组
|
|
81
|
+
'.xq-news-tl{position:relative;padding-left:14px;}\n' +
|
|
82
|
+
'.xq-news-tl::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;width:1px;background:var(--dsw-alias-border-l2);}\n' +
|
|
83
|
+
'.xq-news-group{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin:8px 0 5px;display:flex;align-items:center;gap:6px;}\n' +
|
|
84
|
+
'.xq-news-group::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l2);margin-left:-13px;flex:none;}\n' +
|
|
85
|
+
'.xq-news-item{position:relative;}\n' +
|
|
86
|
+
// 徽章 hover 预览弹层
|
|
87
|
+
'.xq-badge-pop{position:fixed;z-index:1201;min-width:210px;max-width:280px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.28);padding:8px 10px;font-size:12px;}\n' +
|
|
88
|
+
'.xq-badge-pop-hd{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin-bottom:5px;display:flex;justify-content:space-between;}\n' +
|
|
89
|
+
'.xq-bp-row{display:flex;align-items:center;gap:8px;padding:2.5px 0;cursor:pointer;border-radius:5px;}\n' +
|
|
90
|
+
'.xq-bp-row:hover{background:var(--dsw-alias-bg-layer-2);}\n' +
|
|
91
|
+
'.xq-bp-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n' +
|
|
92
|
+
'.xq-bp-val{font-weight:600;}\n' +
|
|
63
93
|
'.xq-grid{display:grid;gap:0;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;margin-bottom:8px;}\n' +
|
|
64
94
|
'.xq-grid-hd,.xq-grid-row{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr 84px;align-items:center;}\n' +
|
|
65
95
|
'.xq-grid-hd{font-size:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);padding:5px 8px;}\n' +
|
|
@@ -263,8 +293,8 @@ exports.default = {
|
|
|
263
293
|
const rows = props.rows || []
|
|
264
294
|
const [hi, setHi] = React.useState(null)
|
|
265
295
|
if (!rows.length) return el('div', { className: 'xq-muted' }, '暂无K线数据')
|
|
266
|
-
const W = 640, MAIN = 150, VOL = 44, PAD = 6, GAP = 4
|
|
267
|
-
const H = MAIN + VOL + GAP + PAD
|
|
296
|
+
const W = 640, MAIN = 150, VOL = 44, PAD = 6, GAP = 4, LBL = 14
|
|
297
|
+
const H = MAIN + VOL + GAP + PAD + LBL
|
|
268
298
|
let min = Infinity, max = -Infinity
|
|
269
299
|
for (let i = 0; i < rows.length; i++) {
|
|
270
300
|
const rlo = Number(rows[i].low), rhi = Number(rows[i].high)
|
|
@@ -278,7 +308,7 @@ exports.default = {
|
|
|
278
308
|
const bw = Math.max(step * 0.66, 1)
|
|
279
309
|
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (MAIN - PAD * 2) }
|
|
280
310
|
const vTop = PAD + MAIN + GAP
|
|
281
|
-
const vBottom = H - PAD
|
|
311
|
+
const vBottom = H - PAD - LBL
|
|
282
312
|
const vH = vBottom - vTop
|
|
283
313
|
let vmax = 0
|
|
284
314
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -338,9 +368,18 @@ exports.default = {
|
|
|
338
368
|
const r = rows[hi]
|
|
339
369
|
const cx = PAD + step * hi + step / 2
|
|
340
370
|
const cy = yOf(Number(r.close))
|
|
371
|
+
// 轴标签:光标价位(右侧底色块)+ 日期(底部底色块)
|
|
372
|
+
const priceTxt = fmt(Number(r.close))
|
|
373
|
+
const pLblW = priceTxt.length * 5.6 + 8
|
|
374
|
+
const dateTxt = fmtFullDay(r.timestamp).slice(5) // MM-DD
|
|
375
|
+
const dLblW = dateTxt.length * 5.6 + 8
|
|
341
376
|
cross = [
|
|
342
377
|
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2: vBottom, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
343
|
-
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 })
|
|
378
|
+
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
379
|
+
el('rect', { key: 'pbg', x: W - PAD - pLblW, y: cy - 7, width: pLblW, height: 14, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
380
|
+
el('text', { key: 'ptx', x: W - PAD - pLblW / 2, y: cy + 3.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, priceTxt),
|
|
381
|
+
el('rect', { key: 'dbg', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW), y: vBottom + 1, width: dLblW, height: 13, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
382
|
+
el('text', { key: 'dtx', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW) + dLblW / 2, y: vBottom + 10.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, dateTxt)
|
|
344
383
|
]
|
|
345
384
|
tip = el('div', {
|
|
346
385
|
key: 'tip', className: 'xq-tip',
|
|
@@ -398,7 +437,7 @@ exports.default = {
|
|
|
398
437
|
const [hi, setHi] = React.useState(null)
|
|
399
438
|
const lastClose = Number(props.lastClose)
|
|
400
439
|
if (!items.length) return el('div', { className: 'xq-muted' }, '暂无分时数据')
|
|
401
|
-
const W = 640,
|
|
440
|
+
const W = 640, CH = 170, LBL = 14, H = CH + LBL, PAD = 6 // CH 图区 + LBL 底部时间标签条
|
|
402
441
|
let min = Infinity, max = -Infinity
|
|
403
442
|
for (let i = 0; i < items.length; i++) {
|
|
404
443
|
const c = Number(items[i].current)
|
|
@@ -408,7 +447,7 @@ exports.default = {
|
|
|
408
447
|
if (lastClose) { if (lastClose < min) min = lastClose; if (lastClose > max) max = lastClose }
|
|
409
448
|
if (!isFinite(min) || !isFinite(max) || min === max) { min -= 1; max += 1 }
|
|
410
449
|
const range = max - min
|
|
411
|
-
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (
|
|
450
|
+
const yOf = function (v) { return PAD + (1 - (v - min) / range) * (CH - PAD * 2) }
|
|
412
451
|
const xOf = function (i) { return PAD + (i / (items.length - 1)) * (W - PAD * 2) }
|
|
413
452
|
const pricePts = []
|
|
414
453
|
const avgPts = []
|
|
@@ -436,9 +475,18 @@ exports.default = {
|
|
|
436
475
|
const it = items[hi]
|
|
437
476
|
const cx = xOf(hi), cy = yOf(Number(it.current))
|
|
438
477
|
const pct = lastClose ? (Number(it.current) - lastClose) / lastClose * 100 : null
|
|
478
|
+
// 轴标签:光标价位(右侧)+ 时间(底部)
|
|
479
|
+
const priceTxt = fmt(Number(it.current))
|
|
480
|
+
const pLblW = priceTxt.length * 5.6 + 8
|
|
481
|
+
const timeTxt = it.timestamp ? fmtTime(it.timestamp) : ''
|
|
482
|
+
const dLblW = timeTxt.length * 5.6 + 8
|
|
439
483
|
cross = [
|
|
440
|
-
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2:
|
|
441
|
-
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 })
|
|
484
|
+
el('line', { key: 'cv', x1: cx, y1: PAD, x2: cx, y2: CH - PAD, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
485
|
+
el('line', { key: 'ch', x1: PAD, y1: cy, x2: W - PAD, y2: cy, stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 0.6, strokeDasharray: '3 3', opacity: 0.7 }),
|
|
486
|
+
el('rect', { key: 'pbg', x: W - PAD - pLblW, y: cy - 7, width: pLblW, height: 14, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
487
|
+
el('text', { key: 'ptx', x: W - PAD - pLblW / 2, y: cy + 3.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, priceTxt),
|
|
488
|
+
el('rect', { key: 'dbg', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW), y: CH + 1, width: dLblW, height: 13, rx: 3, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 0.5 }),
|
|
489
|
+
el('text', { key: 'dtx', x: Math.min(Math.max(PAD, cx - dLblW / 2), W - PAD - dLblW) + dLblW / 2, y: CH + 10.5, fontSize: 9, textAnchor: 'middle', fill: 'var(--dsw-alias-label-primary)' }, timeTxt)
|
|
442
490
|
]
|
|
443
491
|
tip = el('div', {
|
|
444
492
|
key: 'tip', className: 'xq-tip',
|
|
@@ -478,6 +526,9 @@ exports.default = {
|
|
|
478
526
|
function XueqiuPanel() {
|
|
479
527
|
const [watchlist, setWatchlist] = React.useState([])
|
|
480
528
|
const [quotes, setQuotes] = React.useState([])
|
|
529
|
+
// 价格闪烁:记录上一价与变化方向(symbol -> 1|-1),行渲染时挂一次性动画类
|
|
530
|
+
const prevPrices = React.useRef({})
|
|
531
|
+
const [flashDir, setFlashDir] = React.useState({})
|
|
481
532
|
const [indices, setIndices] = React.useState([])
|
|
482
533
|
const [hot, setHot] = React.useState([])
|
|
483
534
|
const [hotMarket, setHotMarket] = React.useState('cn')
|
|
@@ -514,7 +565,15 @@ exports.default = {
|
|
|
514
565
|
setErr('')
|
|
515
566
|
if (!wl.length) return
|
|
516
567
|
return call('quote', { symbols: wl }).then(function (data) {
|
|
517
|
-
|
|
568
|
+
const list = (data && data.list) || []
|
|
569
|
+
const dirs = {}
|
|
570
|
+
list.forEach(function (q) {
|
|
571
|
+
const prev = prevPrices.current[q.symbol]
|
|
572
|
+
if (prev !== undefined && q.current !== prev) dirs[q.symbol] = q.current > prev ? 1 : -1
|
|
573
|
+
prevPrices.current[q.symbol] = q.current
|
|
574
|
+
})
|
|
575
|
+
if (Object.keys(dirs).length) setFlashDir(dirs)
|
|
576
|
+
setQuotes(list)
|
|
518
577
|
})
|
|
519
578
|
}).catch(function (e) {
|
|
520
579
|
setErr(String((e && e.message) || e))
|
|
@@ -675,12 +734,12 @@ exports.default = {
|
|
|
675
734
|
}).concat([
|
|
676
735
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
677
736
|
el('button', {
|
|
678
|
-
key: 'btn', className: 'xq-btn', disabled: loading,
|
|
737
|
+
key: 'btn', className: 'xq-btn', disabled: loading, title: '刷新全部数据',
|
|
679
738
|
onClick: function () {
|
|
680
739
|
setLoading(true)
|
|
681
740
|
Promise.all([refreshMarket(), refreshContent()]).then(function () { setLoading(false) }).catch(function () { setLoading(false) })
|
|
682
741
|
}
|
|
683
|
-
}, '刷新')
|
|
742
|
+
}, loading ? [el('span', { key: 's', className: 'xq-refresh-spin' }, '⟳'), ' 刷新中'] : '刷新')
|
|
684
743
|
]))
|
|
685
744
|
|
|
686
745
|
const errBox = err ? el('div', { className: 'xq-err' }, err) : null
|
|
@@ -708,7 +767,7 @@ exports.default = {
|
|
|
708
767
|
el('div', { key: 'n', className: 'xq-idx-name' }, q.name),
|
|
709
768
|
el('div', { key: 'r', className: 'xq-idx-row' }, [
|
|
710
769
|
el('span', { key: 'c', className: 'xq-idx-cur ' + colorOf(q.percent) }, fmt(q.current)),
|
|
711
|
-
el('span', { key: 'p', className: 'xq-idx-pct ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
770
|
+
el('span', { key: 'p', className: 'xq-idx-pct xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
712
771
|
])
|
|
713
772
|
])
|
|
714
773
|
})
|
|
@@ -718,8 +777,8 @@ exports.default = {
|
|
|
718
777
|
el('div', { key: 'a', className: 'xq-name' }, q.name),
|
|
719
778
|
el('div', { key: 'b', className: 'xq-sub' }, q.symbol)
|
|
720
779
|
]),
|
|
721
|
-
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
722
|
-
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
780
|
+
el('div', { key: 'c', className: colorOf(q.percent) + (flashDir[q.symbol] === 1 ? ' xq-flash-up' : flashDir[q.symbol] === -1 ? ' xq-flash-down' : '') }, fmt(q.current)),
|
|
781
|
+
el('div', { key: 'p' }, el('span', { key: 'pp', className: 'xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))),
|
|
723
782
|
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
724
783
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
725
784
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); openDetail(q.symbol) } }, '详情'),
|
|
@@ -905,11 +964,25 @@ exports.default = {
|
|
|
905
964
|
}
|
|
906
965
|
|
|
907
966
|
function NewsTab() {
|
|
908
|
-
|
|
909
|
-
|
|
967
|
+
// 时间轴:按「今天/昨天/M月D日」分组,每组一个锚点头
|
|
968
|
+
const kids = []
|
|
969
|
+
let lastDay = null
|
|
970
|
+
news.forEach(function (it) {
|
|
971
|
+
const d = new Date(it.created_at)
|
|
972
|
+
const now = new Date()
|
|
973
|
+
const sameDay = function (a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() }
|
|
974
|
+
const yest = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
|
|
975
|
+
let label = null
|
|
976
|
+
const dayKey = d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate()
|
|
977
|
+
if (dayKey !== lastDay) {
|
|
978
|
+
lastDay = dayKey
|
|
979
|
+
label = sameDay(d, now) ? '今天' : (sameDay(d, yest) ? '昨天' : (d.getMonth() + 1) + '月' + d.getDate() + '日')
|
|
980
|
+
}
|
|
981
|
+
if (label) kids.push(el('div', { key: 'g' + dayKey, className: 'xq-news-group' }, label))
|
|
982
|
+
kids.push(el('div', { key: String(it.id), className: 'xq-news-item' + (it.mark === 1 ? ' xq-important' : '') }, [
|
|
910
983
|
el('div', { key: 't' }, it.text),
|
|
911
984
|
el('div', { key: 'm', className: 'xq-news-time' }, fmtTime(it.created_at) + (it.mark === 1 ? ' · 重要' : ''))
|
|
912
|
-
])
|
|
985
|
+
]))
|
|
913
986
|
})
|
|
914
987
|
// 滚动到底自动加载;手动入口兜底(滚轮惯性/触控板场景)
|
|
915
988
|
kids.push(el('div', {
|
|
@@ -919,7 +992,7 @@ exports.default = {
|
|
|
919
992
|
}, newsMore ? '加载中…' : (newsNoMore ? '没有更早的快讯了' : '加载更早 ↑')))
|
|
920
993
|
return news.length
|
|
921
994
|
? el('div', {
|
|
922
|
-
className: 'xq-news',
|
|
995
|
+
className: 'xq-news xq-news-tl',
|
|
923
996
|
onScroll: function (e) {
|
|
924
997
|
const n = e.target
|
|
925
998
|
if (n.scrollTop + n.clientHeight >= n.scrollHeight - 40) loadMoreNews()
|
|
@@ -941,7 +1014,8 @@ exports.default = {
|
|
|
941
1014
|
content = NewsTab()
|
|
942
1015
|
}
|
|
943
1016
|
|
|
944
|
-
|
|
1017
|
+
// key 变化触发 xq-fade-in 重放:tab/详情切换有轻微淡入
|
|
1018
|
+
return el('div', null, [tabs, errBox, el('div', { key: 'v-' + (view ? 'd' : tab), className: 'xq-view' }, content)])
|
|
945
1019
|
}
|
|
946
1020
|
|
|
947
1021
|
// ---------- 嵌入式主面板(conversation.input.dock):输入框上方一整行 ----------
|
|
@@ -1047,6 +1121,41 @@ exports.default = {
|
|
|
1047
1121
|
const [idx, setIdx] = React.useState([])
|
|
1048
1122
|
const [mOpen, setMOpen] = React.useState(true)
|
|
1049
1123
|
const [, force] = React.useState(0)
|
|
1124
|
+
// hover 预览:延迟出现,拖拽/点击不误触;数据 30s 内复用
|
|
1125
|
+
const [pop, setPop] = React.useState(null) // { quotes: [], at: ts }
|
|
1126
|
+
const hoverTimer = React.useRef(null)
|
|
1127
|
+
const popCache = React.useRef({ at: 0, list: [] })
|
|
1128
|
+
|
|
1129
|
+
function loadPop() {
|
|
1130
|
+
const now = Date.now()
|
|
1131
|
+
if (now - popCache.current.at < 30000 && popCache.current.list.length) {
|
|
1132
|
+
setPop({ quotes: popCache.current.list, at: popCache.current.at })
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1135
|
+
// 两次 RPC(watchlist + quotes),走既有节流管线
|
|
1136
|
+
call('watchlist.get', {}).then(function (wl) {
|
|
1137
|
+
const symbols = (wl && wl.symbols) || []
|
|
1138
|
+
if (!symbols.length) { setPop({ quotes: [], at: now }); return }
|
|
1139
|
+
return call('quote', { symbols: symbols }).then(function (data) {
|
|
1140
|
+
const list = (data && data.list) || []
|
|
1141
|
+
popCache.current = { at: now, list: list }
|
|
1142
|
+
setPop({ quotes: list, at: now })
|
|
1143
|
+
})
|
|
1144
|
+
}).catch(function () { /* 静默 */ })
|
|
1145
|
+
}
|
|
1146
|
+
function onEnter() {
|
|
1147
|
+
if (hoverTimer.current) return
|
|
1148
|
+
hoverTimer.current = setTimeout(function () {
|
|
1149
|
+
hoverTimer.current = null
|
|
1150
|
+
if (!badgeDrag) loadPop()
|
|
1151
|
+
}, 300)
|
|
1152
|
+
}
|
|
1153
|
+
function onLeave() {
|
|
1154
|
+
if (hoverTimer.current) { clearTimeout(hoverTimer.current); hoverTimer.current = null }
|
|
1155
|
+
setPop(null)
|
|
1156
|
+
}
|
|
1157
|
+
React.useEffect(function () { return function () { if (hoverTimer.current) clearTimeout(hoverTimer.current) } }, [])
|
|
1158
|
+
|
|
1050
1159
|
React.useEffect(function () {
|
|
1051
1160
|
let alive = true
|
|
1052
1161
|
function refresh() {
|
|
@@ -1110,11 +1219,37 @@ exports.default = {
|
|
|
1110
1219
|
const style = ui.badgePos
|
|
1111
1220
|
? { left: ui.badgePos.x, top: ui.badgePos.y, right: 'auto', bottom: 'auto' }
|
|
1112
1221
|
: null
|
|
1113
|
-
|
|
1222
|
+
// hover 预览弹层:跟随徽章位置(有拖拽坐标用之,否则用默认右下角),显示在徽章上方
|
|
1223
|
+
let popStyle = null
|
|
1224
|
+
if (ui.badgePos) {
|
|
1225
|
+
popStyle = { left: Math.max(6, ui.badgePos.x - 60), top: Math.max(6, ui.badgePos.y - 210), right: 'auto', bottom: 'auto' }
|
|
1226
|
+
} else {
|
|
1227
|
+
popStyle = { right: 16, bottom: 104, left: 'auto', top: 'auto' }
|
|
1228
|
+
}
|
|
1229
|
+
let popEl = null
|
|
1230
|
+
if (pop) {
|
|
1231
|
+
const rows = pop.quotes.slice(0, 8).map(function (q) {
|
|
1232
|
+
return el('div', {
|
|
1233
|
+
key: q.symbol, className: 'xq-bp-row', title: '点击查看详情',
|
|
1234
|
+
onClick: function (e) { e.stopPropagation(); setPop(null); ui.set({ open: true }) }
|
|
1235
|
+
}, [
|
|
1236
|
+
el('span', { key: 'n', className: 'xq-bp-name' }, q.name || q.symbol),
|
|
1237
|
+
el('span', { key: 'v', className: 'xq-bp-val ' + colorOf(q.percent) }, fmt(q.current)),
|
|
1238
|
+
el('span', { key: 'p', className: 'xq-pct-chip ' + colorOf(q.percent) }, fmtPct(q.percent))
|
|
1239
|
+
])
|
|
1240
|
+
})
|
|
1241
|
+
popEl = el('div', { key: 'pop', className: 'xq-badge-pop', style: popStyle, onMouseEnter: onEnter, onMouseLeave: onLeave }, [
|
|
1242
|
+
el('div', { key: 'hd', className: 'xq-badge-pop-hd' }, [el('span', { key: 't' }, '自选 · ' + pop.quotes.length + ' 只'), el('span', { key: 'h' }, '悬停查看 · 点击展开')]),
|
|
1243
|
+
rows.length ? rows : el('div', { key: 'e', className: 'xq-muted' }, '自选为空')
|
|
1244
|
+
])
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const badgeEl = el('div', {
|
|
1114
1248
|
className: 'xq-badge',
|
|
1115
1249
|
style: style,
|
|
1116
|
-
title: '点击开合行情面板 · 拖动调整位置',
|
|
1117
|
-
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp
|
|
1250
|
+
title: '点击开合行情面板 · 拖动调整位置 · 悬停速览自选',
|
|
1251
|
+
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp,
|
|
1252
|
+
onMouseEnter: onEnter, onMouseLeave: onLeave
|
|
1118
1253
|
}, [
|
|
1119
1254
|
el('span', { key: 'logo' }, [el('b', { key: 'b' }, '雪球'), 'mini']),
|
|
1120
1255
|
el('span', { key: 'st', className: 'xq-status' + (aSession() === '盘中' ? ' xq-live' : ''), title: sessionsText() }, (function () { var s = aSession(); return s === '盘中' ? '● 盘中' : (s || (mOpen ? '● 盘中' : '休市')) })()),
|
|
@@ -1129,6 +1264,8 @@ exports.default = {
|
|
|
1129
1264
|
]) : null,
|
|
1130
1265
|
el('span', { key: 'ct', className: 'xq-caret' }, ui.open ? '▾' : '▴')
|
|
1131
1266
|
])
|
|
1267
|
+
// 徽章 + 弹层同层渲染(都为 fixed 定位,互不嵌套,拖拽事件不穿透到弹层)
|
|
1268
|
+
return el('div', { style: { contents: 'display' } }, popEl ? [badgeEl, popEl] : [badgeEl])
|
|
1132
1269
|
}
|
|
1133
1270
|
|
|
1134
1271
|
// ---------- 底部指数条(会话页,输入框下方氛围行) ----------
|