dsh-xueqiu 1.6.0 → 1.7.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.md +30 -0
- package/dynamic/client.js +64 -8
- package/dynamic/host.js +31 -8
- package/package.json +2 -2
- package/src/client/index.js +64 -8
- package/src/index.js +31 -8
package/README.md
CHANGED
|
@@ -20,8 +20,12 @@
|
|
|
20
20
|
| 💼 自选股 | 本地持久化,增删随点随改 |
|
|
21
21
|
| 🧲 嵌入式面板 | 完整面板停靠在**输入框上方**(官方 `conversation.input.dock` 槽位),随页面布局流动,**不遮挡对话记录** |
|
|
22
22
|
| 🏷️ 迷你徽章 | 常驻悬浮徽章显示**上证/深成指实时涨跌**与盘中状态;点击开合面板,整体可拖动到任意位置(位置记忆) |
|
|
23
|
+
| 📏 面板高度可调 | 拖动面板底边调整高度(160px–85% 视口,双击复位),高度持久化记忆 |
|
|
23
24
|
| ⌨️ Esc 收起 | Esc 先关个股详情,再收起面板;点徽章或底部指数条重新展开 |
|
|
25
|
+
| 🛡️ 请求防护 | 并发 2 + 100ms 最小间隔对齐网页端行为;**30s 看门狗**强制释放悬挂请求防管线冻结;Cookie 失效/风控自动重播种重试;限频指数退避;**页面隐藏自动暂停全部轮询**(回前台立即恢复) |
|
|
26
|
+
| 🗂️ 渐进详情 | 个股详情报价+K线先上屏,分时/财务/KOL 到达后增量合并,不用等齐 |
|
|
24
27
|
| ⏱️ 智能刷新 | 盘中 20s 刷新行情,收盘自动放慢,降低被风控概率 |
|
|
28
|
+
| 🕐 交易时段 | 面板头常驻显示 **A 股/港股/美股** 盘中·午休·盘前·休市(本地时区推算,不含节假日),徽章显示精确 A 股时段 |
|
|
25
29
|
| 🌗 主题自适应 | 跟随 DSH 明暗主题 |
|
|
26
30
|
|
|
27
31
|
所有数据来自雪球公开接口(访问首页获取匿名 cookie + 浏览器 UA/Referer),**无需登录**。
|
|
@@ -74,8 +78,20 @@ dsh plugin --profile web add ./dsh-xueqiu
|
|
|
74
78
|
- **右下角迷你徽章**常驻显示上证/深成指实时涨跌;**点击**开合面板,**拖动**调整位置(记忆位置)。
|
|
75
79
|
- 输入框下方的**指数条**点击也可展开面板。
|
|
76
80
|
- 点击自选股、指数卡或热榜行 → 个股详情:16 项行情数据 + K线/分时切换(悬停图表看十字光标详情)+ 财务指标(ROE/毛利率/净利同比等)+ 热议用户。
|
|
81
|
+
- **面板高度**:拖动面板底边的手柄上下调整(160px ~ 85% 视口高度),**双击手柄复位**;高度会被记忆。
|
|
77
82
|
- 刷新频率:盘中行情 20s、内容 60s;收盘后自动降为 60s / 3min。
|
|
78
83
|
|
|
84
|
+
## 🔧 稳定性设计
|
|
85
|
+
|
|
86
|
+
数据层内置多层防护,长时间挂机也不会卡死:
|
|
87
|
+
|
|
88
|
+
- **请求闸门**:并发上限 2、请求间最小间隔 100ms,模拟网页端节奏,降低风控概率。
|
|
89
|
+
- **30s 看门狗**:单个请求悬挂超 30s 即强制释放调度槽并报错,不会冻结后续所有请求。
|
|
90
|
+
- **TTL 缓存 + in-flight 去重**:相同 URL 短时间窗内直接命中缓存,并发重复请求共享同一 Promise。
|
|
91
|
+
- **Cookie 自愈**:匿名 cookie 失效(错误码 400016)或被风控返回空响应时,自动重新访问雪球首页播种新 cookie 再重试。
|
|
92
|
+
- **限频退避**:遇到"请求频繁"按 2s→4s 指数退避后重试。
|
|
93
|
+
- **渐进渲染**:详情页报价 + K线先行上屏,分时/财务/KOL 异步到达后增量合并。
|
|
94
|
+
|
|
79
95
|
## 📁 目录结构
|
|
80
96
|
|
|
81
97
|
```
|
|
@@ -96,6 +112,20 @@ dsh-xueqiu/
|
|
|
96
112
|
- 数据来自雪球公开 Web 接口,仅供**学习与研究**,**不构成任何投资建议**;请勿高频请求,遵守目标网站条款。
|
|
97
113
|
- 接口可能随时变更导致功能失效,欢迎提 [Issue](https://github.com/kangjinghang/dsh-xueqiu/issues) / PR。
|
|
98
114
|
|
|
115
|
+
## 📋 更新日志
|
|
116
|
+
|
|
117
|
+
- **1.7.0**(2025-08-19)
|
|
118
|
+
- 新增:页面隐藏时自动暂停面板/徽章/指数条全部轮询,回到前台立即刷新——切走不浪费请求,显著降低雪球风控触发概率。
|
|
119
|
+
- 新增:A 股/港股/美股三市场交易时段提示(盘中·午休·盘前·休市,本地 Intl 时区推算,不含节假日),面板头常驻显示,徽章显示精确 A 股时段,每分钟自动刷新。
|
|
120
|
+
- **1.6.1**(2025-08-18)
|
|
121
|
+
- 修复:请求管线死锁——悬挂请求永久占用调度槽导致面板整体冻结,现由 30s 看门狗强制释放。
|
|
122
|
+
- 修复:详情页内点「行情/热榜/搜索/快讯」标签无响应(view 优先级遮蔽 tab)。
|
|
123
|
+
- 修复:徽章位置持久化后恢复时 `badgePos.y` 被误当函数调用导致位置重置失败。
|
|
124
|
+
- 新增:`debug` RPC(running/waiters/inflight/cache/cookie 实时观测)。
|
|
125
|
+
- **1.6.0** — 面板高度拖拽调节(丝滑零重渲染、双击复位、持久化)。
|
|
126
|
+
- **1.5.x** — 请求调度与缓存优化(并发 2 + 100ms 间隔、TTL 缓存、错误分类重试)。
|
|
127
|
+
- **1.4.x** — 详情提速:渐进渲染、全 K线周期、十字光标。
|
|
128
|
+
|
|
99
129
|
## 📄 License
|
|
100
130
|
|
|
101
131
|
[MIT](./LICENSE)
|
package/dynamic/client.js
CHANGED
|
@@ -171,6 +171,36 @@ return {
|
|
|
171
171
|
return null
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function pageHidden() {
|
|
175
|
+
try { return typeof document !== 'undefined' && document.hidden } catch (e) { return false }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 交易时段:本地 Intl 时区判定(不含节假日,仅供参考)
|
|
179
|
+
// segs: [[开盘分,收盘分],...];lunch: [午休起,午休止](可选)
|
|
180
|
+
function sessionState(tz, segs, lunch) {
|
|
181
|
+
try {
|
|
182
|
+
var now = new Date()
|
|
183
|
+
var f = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour12: false, weekday: 'short', hour: '2-digit', minute: '2-digit' })
|
|
184
|
+
var p = {}
|
|
185
|
+
f.formatToParts(now).forEach(function (x) { p[x.type] = x.value })
|
|
186
|
+
if (p.weekday === 'Sat' || p.weekday === 'Sun') return '休市'
|
|
187
|
+
var h = parseInt(p.hour, 10) % 24
|
|
188
|
+
var cur = h * 60 + parseInt(p.minute, 10)
|
|
189
|
+
var inSess = segs.some(function (s) { return cur >= s[0] && cur < s[1] })
|
|
190
|
+
if (inSess) return '盘中'
|
|
191
|
+
if (lunch && cur >= lunch[0] && cur < lunch[1]) return '午休'
|
|
192
|
+
return cur < segs[0][0] ? '盘前' : '休市'
|
|
193
|
+
} catch (e) { return '' }
|
|
194
|
+
}
|
|
195
|
+
function aSession() { return sessionState('Asia/Shanghai', [[570, 690], [780, 900]], [690, 780]) }
|
|
196
|
+
function hkSession() { return sessionState('Asia/Hong_Kong', [[570, 720], [780, 960]], [720, 780]) }
|
|
197
|
+
function usSession() { return sessionState('America/New_York', [[570, 960]], null) }
|
|
198
|
+
function sessionsText() {
|
|
199
|
+
var a = aSession(), hk = hkSession(), us = usSession()
|
|
200
|
+
if (!a && !hk && !us) return ''
|
|
201
|
+
return [a && 'A股' + a, hk && '港股' + hk, us && '美股' + us].filter(Boolean).join(' · ')
|
|
202
|
+
}
|
|
203
|
+
|
|
174
204
|
// ---------- 共享 UI 状态:面板开合 / 当前标签 / 徽章位置 ----------
|
|
175
205
|
const ui = {
|
|
176
206
|
open: false, tab: 'market', badgePos: null, dockH: null, hydrated: false,
|
|
@@ -492,9 +522,17 @@ return {
|
|
|
492
522
|
React.useEffect(function () {
|
|
493
523
|
const marketMs = marketOpen ? 20000 : 60000
|
|
494
524
|
const contentMs = marketOpen ? 60000 : 180000
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
525
|
+
// 页面隐藏时暂停轮询(省请求、降低风控概率),回到前台立即刷新一次
|
|
526
|
+
const stopA = ctx.interval(function () { if (!pageHidden()) refreshMarket() }, marketMs)
|
|
527
|
+
const stopB = ctx.interval(function () { if (!pageHidden()) refreshContent() }, contentMs)
|
|
528
|
+
function onVis() {
|
|
529
|
+
if (!pageHidden()) { refreshMarket(); refreshContent() }
|
|
530
|
+
}
|
|
531
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
532
|
+
return function () {
|
|
533
|
+
if (stopA) stopA(); if (stopB) stopB()
|
|
534
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
535
|
+
}
|
|
498
536
|
}, [marketOpen])
|
|
499
537
|
|
|
500
538
|
React.useEffect(function () {
|
|
@@ -589,7 +627,7 @@ return {
|
|
|
589
627
|
].map(function (t) {
|
|
590
628
|
return el('button', {
|
|
591
629
|
key: t[0], className: 'xq-tab' + (tab === t[0] ? ' xq-tab-active' : ''),
|
|
592
|
-
onClick: function () { setTab(t[0]) }
|
|
630
|
+
onClick: function () { setTab(t[0]); if (view) setView(null) }
|
|
593
631
|
}, t[1])
|
|
594
632
|
}).concat([
|
|
595
633
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
@@ -879,10 +917,16 @@ return {
|
|
|
879
917
|
|
|
880
918
|
function DockPanel() {
|
|
881
919
|
const [, force] = React.useState(0)
|
|
920
|
+
const [tick, setTick] = React.useState(0)
|
|
882
921
|
React.useEffect(function () {
|
|
883
922
|
const off = ui.subscribe(function () { if (ui.hydrated) saveUi(); force(function (x) { return x + 1 }) })
|
|
884
923
|
return off
|
|
885
924
|
}, [])
|
|
925
|
+
// 每分钟重算一次交易时段提示
|
|
926
|
+
React.useEffect(function () {
|
|
927
|
+
const stop = ctx.interval(function () { setTick(function (x) { return x + 1 }) }, 60000)
|
|
928
|
+
return function () { if (stop) stop() }
|
|
929
|
+
}, [])
|
|
886
930
|
function onDown(e) {
|
|
887
931
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
888
932
|
dockResize.active = true
|
|
@@ -927,7 +971,7 @@ return {
|
|
|
927
971
|
return el('div', { className: 'xq-dock' }, [
|
|
928
972
|
el('div', { key: 'head', className: 'xq-dock-head' }, [
|
|
929
973
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
930
|
-
el('span', { key: 'st', className: 'xq-status' }, '行情面板'),
|
|
974
|
+
el('span', { key: 'st', className: 'xq-status', title: 'A股/港股/美股交易时段(本地时区推算,不含节假日)' }, sessionsText() || '行情面板'),
|
|
931
975
|
el('span', { key: 'hint', className: 'xq-update' }, '已嵌入输入框上方 · Esc 收起'),
|
|
932
976
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
933
977
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起(点右下角徽章重新打开)', onClick: function () { ui.set({ open: false }) } }, '收起 —')
|
|
@@ -951,6 +995,7 @@ return {
|
|
|
951
995
|
React.useEffect(function () {
|
|
952
996
|
let alive = true
|
|
953
997
|
function refresh() {
|
|
998
|
+
if (pageHidden()) return // 页面隐藏时暂停徽章轮询
|
|
954
999
|
call('quote', { symbols: ['SH000001', 'SZ399001'] }).then(function (data) {
|
|
955
1000
|
if (!alive) return
|
|
956
1001
|
setIdx((data && data.list) || [])
|
|
@@ -958,10 +1003,15 @@ return {
|
|
|
958
1003
|
setMOpen(st === 5 || st === 6)
|
|
959
1004
|
}).catch(function () { /* 静默失败 */ })
|
|
960
1005
|
}
|
|
1006
|
+
function onVis() { if (!pageHidden()) refresh() }
|
|
961
1007
|
refresh()
|
|
962
1008
|
const stop = ctx.interval(refresh, 30000)
|
|
963
1009
|
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
964
|
-
|
|
1010
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1011
|
+
return function () {
|
|
1012
|
+
alive = false; if (stop) stop(); off()
|
|
1013
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1014
|
+
}
|
|
965
1015
|
}, [])
|
|
966
1016
|
|
|
967
1017
|
function onDown(e) {
|
|
@@ -1012,7 +1062,7 @@ return {
|
|
|
1012
1062
|
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp
|
|
1013
1063
|
}, [
|
|
1014
1064
|
el('span', { key: 'logo' }, [el('b', { key: 'b' }, '雪球'), 'mini']),
|
|
1015
|
-
el('span', { key: 'st', className: 'xq-status' + (
|
|
1065
|
+
el('span', { key: 'st', className: 'xq-status' + (aSession() === '盘中' ? ' xq-live' : ''), title: sessionsText() }, (function () { var s = aSession(); return s === '盘中' ? '● 盘中' : (s || (mOpen ? '● 盘中' : '休市')) })()),
|
|
1016
1066
|
sh ? el('span', { key: 'sh' }, [
|
|
1017
1067
|
el('span', { key: 'n', className: 'xq-badge-hint' }, sh.name + ' '),
|
|
1018
1068
|
el('span', { key: 'v', className: 'xq-badge-val ' + colorOf(sh.percent) }, fmt(sh.current)),
|
|
@@ -1032,13 +1082,19 @@ return {
|
|
|
1032
1082
|
React.useEffect(function () {
|
|
1033
1083
|
let alive = true
|
|
1034
1084
|
function refresh() {
|
|
1085
|
+
if (pageHidden()) return // 页面隐藏时暂停指数条轮询
|
|
1035
1086
|
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1036
1087
|
if (alive) setIndices((data && data.list) || [])
|
|
1037
1088
|
}).catch(function () { /* 静默失败 */ })
|
|
1038
1089
|
}
|
|
1090
|
+
function onVis() { if (!pageHidden()) refresh() }
|
|
1039
1091
|
refresh()
|
|
1040
1092
|
const stop = ctx.interval(refresh, 30000)
|
|
1041
|
-
|
|
1093
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1094
|
+
return function () {
|
|
1095
|
+
alive = false; if (stop) stop()
|
|
1096
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1097
|
+
}
|
|
1042
1098
|
}, [])
|
|
1043
1099
|
if (!indices.length) return null
|
|
1044
1100
|
const kids = indices.map(function (q) {
|
package/dynamic/host.js
CHANGED
|
@@ -32,18 +32,38 @@ return {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// 看门狗:单个请求最长 30s,超时强制释放槽位,防止悬挂请求冻结整条管线
|
|
36
|
+
const GATE_TIMEOUT_MS = 30000
|
|
37
|
+
|
|
35
38
|
function gate(fn) {
|
|
36
39
|
return new Promise(function (resolve, reject) {
|
|
37
40
|
waiters.push(async function () {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
let released = false
|
|
42
|
+
function rel() {
|
|
43
|
+
if (released) return
|
|
44
|
+
released = true
|
|
45
|
+
running--
|
|
46
|
+
pump()
|
|
47
|
+
}
|
|
48
|
+
let settled = false
|
|
49
|
+
let watchdog = null
|
|
50
|
+
function settle(fnName, arg) {
|
|
51
|
+
if (settled) return
|
|
52
|
+
settled = true
|
|
53
|
+
if (watchdog) { try { watchdog() } catch (e) { /* ignore */ } }
|
|
54
|
+
if (fnName === 'resolve') resolve(arg)
|
|
55
|
+
else reject(arg)
|
|
56
|
+
}
|
|
57
|
+
try { watchdog = ctx.timeout(function () { rel(); settle('reject', new Error('[timeout] 请求超时(' + GATE_TIMEOUT_MS + 'ms),已释放调度槽')) }, GATE_TIMEOUT_MS) } catch (e) { /* ignore */ }
|
|
41
58
|
try {
|
|
42
|
-
|
|
59
|
+
const wait = lastStart + MIN_GAP_MS - Date.now()
|
|
60
|
+
if (wait > 0) await delay(wait)
|
|
61
|
+
lastStart = Date.now()
|
|
62
|
+
settle('resolve', await fn())
|
|
43
63
|
} catch (e) {
|
|
44
|
-
reject
|
|
64
|
+
settle('reject', e)
|
|
45
65
|
} finally {
|
|
46
|
-
|
|
66
|
+
rel()
|
|
47
67
|
}
|
|
48
68
|
})
|
|
49
69
|
pump()
|
|
@@ -490,7 +510,7 @@ return {
|
|
|
490
510
|
return {
|
|
491
511
|
tab: s.tab || 'market', open: s.open !== false,
|
|
492
512
|
dockH: (s.dockH !== undefined && isFinite(hNum) && hNum >= 160 && hNum <= 1200) ? hNum : null,
|
|
493
|
-
badgePos: (s.badgePos && isFinite(Number(s.badgePos.x)) && isFinite(Number(s.badgePos.y
|
|
513
|
+
badgePos: (s.badgePos && isFinite(Number(s.badgePos.x)) && isFinite(Number(s.badgePos.y)))
|
|
494
514
|
? { x: Number(s.badgePos.x), y: Number(s.badgePos.y) } : null
|
|
495
515
|
}
|
|
496
516
|
}
|
|
@@ -516,7 +536,10 @@ return {
|
|
|
516
536
|
hot: actHot, search: actSearch, searchPosts: actSearchPosts, news: actNews,
|
|
517
537
|
kol: actKOL, user: actUser, finance: actFinance,
|
|
518
538
|
'watchlist.get': actWatchlistGet, 'watchlist.add': actWatchlistAdd, 'watchlist.remove': actWatchlistRemove,
|
|
519
|
-
'ui.get': actUiGet, 'ui.set': actUiSet
|
|
539
|
+
'ui.get': actUiGet, 'ui.set': actUiSet,
|
|
540
|
+
debug: async function () {
|
|
541
|
+
return { running: running, waiters: waiters.length, inflight: Array.from(inflight.keys()), cacheKeys: cache.size, cookie: state.cookie ? 'set' : 'none' }
|
|
542
|
+
}
|
|
520
543
|
}
|
|
521
544
|
|
|
522
545
|
ctx.effect(function () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-xueqiu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "雪球 mini 行情面板 — DeepSeek Harness 免登录 A股/港美股实时行情、K线(蜡烛/成交量/均线)、分时图、热榜、搜索、7x24 快讯、热议用户。嵌入式面板 + 可拖拽行情徽标,内置请求节流、TTL 缓存与 Cookie 自动续期。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -47,4 +47,4 @@
|
|
|
47
47
|
"README.md",
|
|
48
48
|
"LICENSE"
|
|
49
49
|
]
|
|
50
|
-
}
|
|
50
|
+
}
|
package/src/client/index.js
CHANGED
|
@@ -171,6 +171,36 @@ return {
|
|
|
171
171
|
return null
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function pageHidden() {
|
|
175
|
+
try { return typeof document !== 'undefined' && document.hidden } catch (e) { return false }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 交易时段:本地 Intl 时区判定(不含节假日,仅供参考)
|
|
179
|
+
// segs: [[开盘分,收盘分],...];lunch: [午休起,午休止](可选)
|
|
180
|
+
function sessionState(tz, segs, lunch) {
|
|
181
|
+
try {
|
|
182
|
+
var now = new Date()
|
|
183
|
+
var f = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour12: false, weekday: 'short', hour: '2-digit', minute: '2-digit' })
|
|
184
|
+
var p = {}
|
|
185
|
+
f.formatToParts(now).forEach(function (x) { p[x.type] = x.value })
|
|
186
|
+
if (p.weekday === 'Sat' || p.weekday === 'Sun') return '休市'
|
|
187
|
+
var h = parseInt(p.hour, 10) % 24
|
|
188
|
+
var cur = h * 60 + parseInt(p.minute, 10)
|
|
189
|
+
var inSess = segs.some(function (s) { return cur >= s[0] && cur < s[1] })
|
|
190
|
+
if (inSess) return '盘中'
|
|
191
|
+
if (lunch && cur >= lunch[0] && cur < lunch[1]) return '午休'
|
|
192
|
+
return cur < segs[0][0] ? '盘前' : '休市'
|
|
193
|
+
} catch (e) { return '' }
|
|
194
|
+
}
|
|
195
|
+
function aSession() { return sessionState('Asia/Shanghai', [[570, 690], [780, 900]], [690, 780]) }
|
|
196
|
+
function hkSession() { return sessionState('Asia/Hong_Kong', [[570, 720], [780, 960]], [720, 780]) }
|
|
197
|
+
function usSession() { return sessionState('America/New_York', [[570, 960]], null) }
|
|
198
|
+
function sessionsText() {
|
|
199
|
+
var a = aSession(), hk = hkSession(), us = usSession()
|
|
200
|
+
if (!a && !hk && !us) return ''
|
|
201
|
+
return [a && 'A股' + a, hk && '港股' + hk, us && '美股' + us].filter(Boolean).join(' · ')
|
|
202
|
+
}
|
|
203
|
+
|
|
174
204
|
// ---------- 共享 UI 状态:面板开合 / 当前标签 / 徽章位置 ----------
|
|
175
205
|
const ui = {
|
|
176
206
|
open: false, tab: 'market', badgePos: null, dockH: null, hydrated: false,
|
|
@@ -492,9 +522,17 @@ return {
|
|
|
492
522
|
React.useEffect(function () {
|
|
493
523
|
const marketMs = marketOpen ? 20000 : 60000
|
|
494
524
|
const contentMs = marketOpen ? 60000 : 180000
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
525
|
+
// 页面隐藏时暂停轮询(省请求、降低风控概率),回到前台立即刷新一次
|
|
526
|
+
const stopA = ctx.interval(function () { if (!pageHidden()) refreshMarket() }, marketMs)
|
|
527
|
+
const stopB = ctx.interval(function () { if (!pageHidden()) refreshContent() }, contentMs)
|
|
528
|
+
function onVis() {
|
|
529
|
+
if (!pageHidden()) { refreshMarket(); refreshContent() }
|
|
530
|
+
}
|
|
531
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
532
|
+
return function () {
|
|
533
|
+
if (stopA) stopA(); if (stopB) stopB()
|
|
534
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
535
|
+
}
|
|
498
536
|
}, [marketOpen])
|
|
499
537
|
|
|
500
538
|
React.useEffect(function () {
|
|
@@ -589,7 +627,7 @@ return {
|
|
|
589
627
|
].map(function (t) {
|
|
590
628
|
return el('button', {
|
|
591
629
|
key: t[0], className: 'xq-tab' + (tab === t[0] ? ' xq-tab-active' : ''),
|
|
592
|
-
onClick: function () { setTab(t[0]) }
|
|
630
|
+
onClick: function () { setTab(t[0]); if (view) setView(null) }
|
|
593
631
|
}, t[1])
|
|
594
632
|
}).concat([
|
|
595
633
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
@@ -879,10 +917,16 @@ return {
|
|
|
879
917
|
|
|
880
918
|
function DockPanel() {
|
|
881
919
|
const [, force] = React.useState(0)
|
|
920
|
+
const [tick, setTick] = React.useState(0)
|
|
882
921
|
React.useEffect(function () {
|
|
883
922
|
const off = ui.subscribe(function () { if (ui.hydrated) saveUi(); force(function (x) { return x + 1 }) })
|
|
884
923
|
return off
|
|
885
924
|
}, [])
|
|
925
|
+
// 每分钟重算一次交易时段提示
|
|
926
|
+
React.useEffect(function () {
|
|
927
|
+
const stop = ctx.interval(function () { setTick(function (x) { return x + 1 }) }, 60000)
|
|
928
|
+
return function () { if (stop) stop() }
|
|
929
|
+
}, [])
|
|
886
930
|
function onDown(e) {
|
|
887
931
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
888
932
|
dockResize.active = true
|
|
@@ -927,7 +971,7 @@ return {
|
|
|
927
971
|
return el('div', { className: 'xq-dock' }, [
|
|
928
972
|
el('div', { key: 'head', className: 'xq-dock-head' }, [
|
|
929
973
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
930
|
-
el('span', { key: 'st', className: 'xq-status' }, '行情面板'),
|
|
974
|
+
el('span', { key: 'st', className: 'xq-status', title: 'A股/港股/美股交易时段(本地时区推算,不含节假日)' }, sessionsText() || '行情面板'),
|
|
931
975
|
el('span', { key: 'hint', className: 'xq-update' }, '已嵌入输入框上方 · Esc 收起'),
|
|
932
976
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
933
977
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起(点右下角徽章重新打开)', onClick: function () { ui.set({ open: false }) } }, '收起 —')
|
|
@@ -951,6 +995,7 @@ return {
|
|
|
951
995
|
React.useEffect(function () {
|
|
952
996
|
let alive = true
|
|
953
997
|
function refresh() {
|
|
998
|
+
if (pageHidden()) return // 页面隐藏时暂停徽章轮询
|
|
954
999
|
call('quote', { symbols: ['SH000001', 'SZ399001'] }).then(function (data) {
|
|
955
1000
|
if (!alive) return
|
|
956
1001
|
setIdx((data && data.list) || [])
|
|
@@ -958,10 +1003,15 @@ return {
|
|
|
958
1003
|
setMOpen(st === 5 || st === 6)
|
|
959
1004
|
}).catch(function () { /* 静默失败 */ })
|
|
960
1005
|
}
|
|
1006
|
+
function onVis() { if (!pageHidden()) refresh() }
|
|
961
1007
|
refresh()
|
|
962
1008
|
const stop = ctx.interval(refresh, 30000)
|
|
963
1009
|
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
964
|
-
|
|
1010
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1011
|
+
return function () {
|
|
1012
|
+
alive = false; if (stop) stop(); off()
|
|
1013
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1014
|
+
}
|
|
965
1015
|
}, [])
|
|
966
1016
|
|
|
967
1017
|
function onDown(e) {
|
|
@@ -1012,7 +1062,7 @@ return {
|
|
|
1012
1062
|
onPointerDown: onDown, onPointerMove: onMove, onPointerUp: onUp, onPointerCancel: onUp
|
|
1013
1063
|
}, [
|
|
1014
1064
|
el('span', { key: 'logo' }, [el('b', { key: 'b' }, '雪球'), 'mini']),
|
|
1015
|
-
el('span', { key: 'st', className: 'xq-status' + (
|
|
1065
|
+
el('span', { key: 'st', className: 'xq-status' + (aSession() === '盘中' ? ' xq-live' : ''), title: sessionsText() }, (function () { var s = aSession(); return s === '盘中' ? '● 盘中' : (s || (mOpen ? '● 盘中' : '休市')) })()),
|
|
1016
1066
|
sh ? el('span', { key: 'sh' }, [
|
|
1017
1067
|
el('span', { key: 'n', className: 'xq-badge-hint' }, sh.name + ' '),
|
|
1018
1068
|
el('span', { key: 'v', className: 'xq-badge-val ' + colorOf(sh.percent) }, fmt(sh.current)),
|
|
@@ -1032,13 +1082,19 @@ return {
|
|
|
1032
1082
|
React.useEffect(function () {
|
|
1033
1083
|
let alive = true
|
|
1034
1084
|
function refresh() {
|
|
1085
|
+
if (pageHidden()) return // 页面隐藏时暂停指数条轮询
|
|
1035
1086
|
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1036
1087
|
if (alive) setIndices((data && data.list) || [])
|
|
1037
1088
|
}).catch(function () { /* 静默失败 */ })
|
|
1038
1089
|
}
|
|
1090
|
+
function onVis() { if (!pageHidden()) refresh() }
|
|
1039
1091
|
refresh()
|
|
1040
1092
|
const stop = ctx.interval(refresh, 30000)
|
|
1041
|
-
|
|
1093
|
+
try { document.addEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1094
|
+
return function () {
|
|
1095
|
+
alive = false; if (stop) stop()
|
|
1096
|
+
try { document.removeEventListener('visibilitychange', onVis) } catch (e) { /* ignore */ }
|
|
1097
|
+
}
|
|
1042
1098
|
}, [])
|
|
1043
1099
|
if (!indices.length) return null
|
|
1044
1100
|
const kids = indices.map(function (q) {
|
package/src/index.js
CHANGED
|
@@ -32,18 +32,38 @@ export default {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// 看门狗:单个请求最长 30s,超时强制释放槽位,防止悬挂请求冻结整条管线
|
|
36
|
+
const GATE_TIMEOUT_MS = 30000
|
|
37
|
+
|
|
35
38
|
function gate(fn) {
|
|
36
39
|
return new Promise(function (resolve, reject) {
|
|
37
40
|
waiters.push(async function () {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
let released = false
|
|
42
|
+
function rel() {
|
|
43
|
+
if (released) return
|
|
44
|
+
released = true
|
|
45
|
+
running--
|
|
46
|
+
pump()
|
|
47
|
+
}
|
|
48
|
+
let settled = false
|
|
49
|
+
let watchdog = null
|
|
50
|
+
function settle(fnName, arg) {
|
|
51
|
+
if (settled) return
|
|
52
|
+
settled = true
|
|
53
|
+
if (watchdog) { try { watchdog() } catch (e) { /* ignore */ } }
|
|
54
|
+
if (fnName === 'resolve') resolve(arg)
|
|
55
|
+
else reject(arg)
|
|
56
|
+
}
|
|
57
|
+
try { watchdog = ctx.timeout(function () { rel(); settle('reject', new Error('[timeout] 请求超时(' + GATE_TIMEOUT_MS + 'ms),已释放调度槽')) }, GATE_TIMEOUT_MS) } catch (e) { /* ignore */ }
|
|
41
58
|
try {
|
|
42
|
-
|
|
59
|
+
const wait = lastStart + MIN_GAP_MS - Date.now()
|
|
60
|
+
if (wait > 0) await delay(wait)
|
|
61
|
+
lastStart = Date.now()
|
|
62
|
+
settle('resolve', await fn())
|
|
43
63
|
} catch (e) {
|
|
44
|
-
reject
|
|
64
|
+
settle('reject', e)
|
|
45
65
|
} finally {
|
|
46
|
-
|
|
66
|
+
rel()
|
|
47
67
|
}
|
|
48
68
|
})
|
|
49
69
|
pump()
|
|
@@ -490,7 +510,7 @@ export default {
|
|
|
490
510
|
return {
|
|
491
511
|
tab: s.tab || 'market', open: s.open !== false,
|
|
492
512
|
dockH: (s.dockH !== undefined && isFinite(hNum) && hNum >= 160 && hNum <= 1200) ? hNum : null,
|
|
493
|
-
badgePos: (s.badgePos && isFinite(Number(s.badgePos.x)) && isFinite(Number(s.badgePos.y
|
|
513
|
+
badgePos: (s.badgePos && isFinite(Number(s.badgePos.x)) && isFinite(Number(s.badgePos.y)))
|
|
494
514
|
? { x: Number(s.badgePos.x), y: Number(s.badgePos.y) } : null
|
|
495
515
|
}
|
|
496
516
|
}
|
|
@@ -516,7 +536,10 @@ export default {
|
|
|
516
536
|
hot: actHot, search: actSearch, searchPosts: actSearchPosts, news: actNews,
|
|
517
537
|
kol: actKOL, user: actUser, finance: actFinance,
|
|
518
538
|
'watchlist.get': actWatchlistGet, 'watchlist.add': actWatchlistAdd, 'watchlist.remove': actWatchlistRemove,
|
|
519
|
-
'ui.get': actUiGet, 'ui.set': actUiSet
|
|
539
|
+
'ui.get': actUiGet, 'ui.set': actUiSet,
|
|
540
|
+
debug: async function () {
|
|
541
|
+
return { running: running, waiters: waiters.length, inflight: Array.from(inflight.keys()), cacheKeys: cache.size, cookie: state.cookie ? 'set' : 'none' }
|
|
542
|
+
}
|
|
520
543
|
}
|
|
521
544
|
|
|
522
545
|
ctx.effect(function () {
|