dsh-xueqiu 1.0.0 → 1.2.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 +10 -4
- package/dynamic/client.js +391 -139
- package/dynamic/host.js +15 -1
- package/package.json +2 -2
- package/src/client/index.js +391 -139
- package/src/index.js +15 -1
package/README.md
CHANGED
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
| 📰 快讯 | 7×24 实时快讯,重要新闻高亮 |
|
|
19
19
|
| 👥 热议用户 | 个股热门 KOL(粉丝数/认证标识) |
|
|
20
20
|
| 💼 自选股 | 本地持久化,增删随点随改 |
|
|
21
|
-
| 🖱️ 悬浮面板 |
|
|
21
|
+
| 🖱️ 悬浮面板 | 按住标题栏自由拖拽,**拖到左/右屏幕边缘自动贴靠为窄栏**(Windows 贴靠风格,380px 可调),**拖到角落自动磁吸**,📌 一键贴右下角/右侧窄栏,**右下角手柄自由缩放宽高** |
|
|
22
|
+
| 🪟 贴靠助手 | 拖动时在屏幕边缘显示**半透明贴靠预览**,松手即吸附;贴靠后拖动标题栏解除 |
|
|
23
|
+
| 📐 窄栏自适应 | 贴靠窄栏自动切换紧凑布局(隐藏次要列、指数卡缩排、K线/表格自适应) |
|
|
24
|
+
| 📌 嵌入模式 | 一键切换到**输入框上方**(页面布局内,**不遮挡对话记录**),宽度可调,随页面滚动 |
|
|
22
25
|
| ⏱️ 智能刷新 | 盘中 20s 刷新行情,收盘自动放慢,降低被风控概率 |
|
|
23
26
|
| 🌗 主题自适应 | 跟随 DSH 明暗主题 |
|
|
24
27
|
|
|
@@ -28,18 +31,21 @@
|
|
|
28
31
|
|
|
29
32
|
**主面板**:大盘指数 + 自选股行情,可拖拽悬浮窗口:
|
|
30
33
|
|
|
31
|
-

|
|
32
35
|
|
|
33
36
|
**个股详情**:16 项行情数据 + K线蜡烛图(成交量柱 / MA5-10-20 均线)+ 财务指标 + 热议用户:
|
|
34
37
|
|
|
35
|
-

|
|
36
39
|
|
|
37
40
|
## 📦 安装
|
|
38
41
|
|
|
39
42
|
### 方式一:标准 bundle 插件(推荐)
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
|
-
#
|
|
45
|
+
# npm 包
|
|
46
|
+
dsh plugin --profile web add dsh-xueqiu
|
|
47
|
+
|
|
48
|
+
# 或 GitHub 源(git 安装会直接从源码构建)
|
|
43
49
|
dsh plugin --profile web add github:kangjinghang/dsh-xueqiu
|
|
44
50
|
|
|
45
51
|
# 或本地目录
|
package/dynamic/client.js
CHANGED
|
@@ -169,13 +169,7 @@ return {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
//
|
|
173
|
-
const saveUi = ctx.debounce(function (s) {
|
|
174
|
-
call('ui.set', s).catch(function () { /* 忽略 */ })
|
|
175
|
-
}, 800)
|
|
176
|
-
|
|
177
|
-
// 拖拽用可变引用(面板实例唯一)
|
|
178
|
-
let dragRef = null
|
|
172
|
+
// (saveUi / dragRef / ui 共享状态见下方新块)
|
|
179
173
|
|
|
180
174
|
// ---------- K线蜡烛图(含成交量与均线) ----------
|
|
181
175
|
function KlineChart(props) {
|
|
@@ -309,61 +303,54 @@ return {
|
|
|
309
303
|
])
|
|
310
304
|
}
|
|
311
305
|
|
|
312
|
-
// ----------
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
306
|
+
// ---------- 共享 UI 状态(悬浮/嵌入/位置/尺寸/标签页) ----------
|
|
307
|
+
const ui = {
|
|
308
|
+
mode: 'floating', pos: null, size: { w: 720, h: 640 }, dockW: 720,
|
|
309
|
+
snap: null, snapEdge: null, snapW: 380,
|
|
310
|
+
minimized: false, tab: 'market', closed: false, hydrated: false,
|
|
311
|
+
_fns: [],
|
|
312
|
+
subscribe: function (fn) {
|
|
313
|
+
this._fns.push(fn)
|
|
314
|
+
const self = this
|
|
315
|
+
return function () {
|
|
316
|
+
const i = self._fns.indexOf(fn)
|
|
317
|
+
if (i !== -1) self._fns.splice(i, 1)
|
|
321
318
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
return el('div', { className: 'xq-ticker', onClick: function () { bus.emit('open') } }, kids)
|
|
319
|
+
},
|
|
320
|
+
notify: function () {
|
|
321
|
+
const fns = this._fns.slice()
|
|
322
|
+
for (let i = 0; i < fns.length; i++) { try { fns[i]() } catch (e) { /* ignore */ } }
|
|
323
|
+
},
|
|
324
|
+
set: function (patch) {
|
|
325
|
+
let ch = false
|
|
326
|
+
for (const k in patch) {
|
|
327
|
+
if (this[k] !== patch[k]) { this[k] = patch[k]; ch = true }
|
|
328
|
+
}
|
|
329
|
+
if (ch) this.notify()
|
|
330
|
+
}
|
|
335
331
|
}
|
|
336
332
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
let alive = true
|
|
342
|
-
function refresh() {
|
|
343
|
-
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
344
|
-
if (alive) setIndices((data && data.list) || [])
|
|
345
|
-
}).catch(function () { /* 静默失败 */ })
|
|
346
|
-
}
|
|
347
|
-
refresh()
|
|
348
|
-
const stop = ctx.interval(refresh, 60000)
|
|
349
|
-
return function () { alive = false; if (stop) stop() }
|
|
350
|
-
}, [])
|
|
351
|
-
const line = indices.map(function (q) {
|
|
352
|
-
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
353
|
-
}).join(' · ')
|
|
354
|
-
return el('div', { className: 'xq-entry', onClick: function () { bus.emit('open') } }, [
|
|
355
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
356
|
-
el('span', { key: 'sum', className: 'xq-sum' }, line || '点击打开行情面板'),
|
|
357
|
-
el('span', { key: 'caret', className: 'xq-caret' }, '打开 ▾')
|
|
358
|
-
])
|
|
333
|
+
function viewport() {
|
|
334
|
+
try { if (typeof window !== 'undefined' && window.innerWidth) return { w: window.innerWidth, h: window.innerHeight } } catch (e) { /* ignore */ }
|
|
335
|
+
try { if (typeof document !== 'undefined' && document.documentElement && document.documentElement.clientWidth) return { w: document.documentElement.clientWidth, h: document.documentElement.clientHeight } } catch (e) { /* ignore */ }
|
|
336
|
+
return null
|
|
359
337
|
}
|
|
360
338
|
|
|
361
|
-
//
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
339
|
+
// 保存 UI 状态(防抖)
|
|
340
|
+
const saveUi = ctx.debounce(function () {
|
|
341
|
+
call('ui.set', {
|
|
342
|
+
mode: ui.mode, pos: ui.pos, size: ui.size, dockW: ui.dockW,
|
|
343
|
+
snap: ui.snap, snapEdge: ui.snapEdge, snapW: ui.snapW,
|
|
344
|
+
tab: ui.tab, minimized: ui.minimized
|
|
345
|
+
}).catch(function () { /* 忽略 */ })
|
|
346
|
+
}, 800)
|
|
347
|
+
|
|
348
|
+
// 拖拽 / 缩放用可变引用(实例唯一)
|
|
349
|
+
let dragRef = null
|
|
350
|
+
let resizeRef = null
|
|
351
|
+
|
|
352
|
+
// ---------- 面板内容(数据 + tabs + 详情),由悬浮/嵌入外壳共用 ----------
|
|
353
|
+
function XueqiuPanel() {
|
|
367
354
|
const [watchlist, setWatchlist] = React.useState([])
|
|
368
355
|
const [quotes, setQuotes] = React.useState([])
|
|
369
356
|
const [indices, setIndices] = React.useState([])
|
|
@@ -383,30 +370,8 @@ return {
|
|
|
383
370
|
const [updatedAt, setUpdatedAt] = React.useState(null)
|
|
384
371
|
const [err, setErr] = React.useState('')
|
|
385
372
|
const [loading, setLoading] = React.useState(true)
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
// 恢复 UI 状态 + 监听 bus
|
|
389
|
-
React.useEffect(function () {
|
|
390
|
-
let alive = true
|
|
391
|
-
call('ui.get', {}).then(function (d) {
|
|
392
|
-
if (!alive) return
|
|
393
|
-
if (d && d.pos) setPos({ x: Number(d.pos.x), y: Number(d.pos.y) })
|
|
394
|
-
if (d && d.tab) setTab(d.tab)
|
|
395
|
-
setMinimized(!!(d && d.minimized))
|
|
396
|
-
setHydrated(true)
|
|
397
|
-
}).catch(function () { setHydrated(true) })
|
|
398
|
-
const off = bus.on(function (ev) {
|
|
399
|
-
if (ev === 'open') { setVisible(true); setMinimized(false) }
|
|
400
|
-
if (ev === 'close') setVisible(false)
|
|
401
|
-
})
|
|
402
|
-
return function () { alive = false; off() }
|
|
403
|
-
}, [])
|
|
404
|
-
|
|
405
|
-
// 持久化 UI 状态(hydrated 后再写,避免覆盖)
|
|
406
|
-
React.useEffect(function () {
|
|
407
|
-
if (!hydrated) return
|
|
408
|
-
saveUi({ tab: tab, minimized: minimized, pos: pos })
|
|
409
|
-
}, [tab, minimized, pos, hydrated])
|
|
373
|
+
const tab = ui.tab
|
|
374
|
+
function setTab(t) { ui.set({ tab: t }) }
|
|
410
375
|
|
|
411
376
|
function refreshMarket() {
|
|
412
377
|
return Promise.all([
|
|
@@ -522,58 +487,6 @@ return {
|
|
|
522
487
|
setManualCode('')
|
|
523
488
|
}
|
|
524
489
|
|
|
525
|
-
// 拖拽
|
|
526
|
-
function onTitleDown(e) {
|
|
527
|
-
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
528
|
-
const baseX = pos ? pos.x : e.clientX
|
|
529
|
-
const baseY = pos ? pos.y : e.clientY
|
|
530
|
-
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
531
|
-
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
532
|
-
}
|
|
533
|
-
function onTitleMove(e) {
|
|
534
|
-
if (!dragRef) return
|
|
535
|
-
setPos({ x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) })
|
|
536
|
-
}
|
|
537
|
-
function onTitleUp(e) {
|
|
538
|
-
dragRef = null
|
|
539
|
-
try {
|
|
540
|
-
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId)
|
|
541
|
-
} catch (err) { /* ignore */ }
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (!visible) return null
|
|
545
|
-
|
|
546
|
-
const dragProps = {
|
|
547
|
-
onPointerDown: onTitleDown,
|
|
548
|
-
onPointerMove: onTitleMove,
|
|
549
|
-
onPointerUp: onTitleUp,
|
|
550
|
-
onPointerCancel: onTitleUp
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 最小化条
|
|
554
|
-
if (minimized) {
|
|
555
|
-
const idxLine = indices.map(function (q) {
|
|
556
|
-
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
557
|
-
}).join(' · ')
|
|
558
|
-
return el('div', { className: 'xq-float xq-min', style: pos ? { left: pos.x, top: pos.y, right: 'auto', bottom: 'auto' } : null, onClick: function () { setMinimized(false) }, ...dragProps }, [
|
|
559
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
560
|
-
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
561
|
-
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
562
|
-
el('button', { key: 'x', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); setVisible(false) }, onPointerDown: function (e) { e.stopPropagation() } }, '✕'),
|
|
563
|
-
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
564
|
-
])
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// 标题栏
|
|
568
|
-
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
569
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
570
|
-
el('span', { key: 'st', className: 'xq-status' + (marketOpen ? ' xq-live' : '') }, marketOpen ? '● 盘中' : '已收盘'),
|
|
571
|
-
el('span', { key: 'upd', className: 'xq-update' }, updatedAt ? ('更新 ' + fmtTime(updatedAt)) : (loading ? '加载中…' : '')),
|
|
572
|
-
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
573
|
-
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); setMinimized(true) } }, '—'),
|
|
574
|
-
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(可从下方入口重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); setVisible(false) } }, '✕')
|
|
575
|
-
])
|
|
576
|
-
|
|
577
490
|
const tabs = el('div', { className: 'xq-tabs' }, [
|
|
578
491
|
['market', '行情'], ['hot', '热榜'], ['search', '搜索'], ['news', '快讯']
|
|
579
492
|
].map(function (t) {
|
|
@@ -613,7 +526,7 @@ return {
|
|
|
613
526
|
]),
|
|
614
527
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
615
528
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
616
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
529
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
617
530
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
618
531
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
619
532
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -818,19 +731,351 @@ return {
|
|
|
818
731
|
content = NewsTab()
|
|
819
732
|
}
|
|
820
733
|
|
|
821
|
-
return el('div',
|
|
822
|
-
|
|
823
|
-
|
|
734
|
+
return el('div', null, [tabs, errBox, content])
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
738
|
+
function FloatingShell() {
|
|
739
|
+
const [, force] = React.useState(0)
|
|
740
|
+
const [fIdx, setFIdx] = React.useState([])
|
|
741
|
+
const [fOpen, setFOpen] = React.useState(true)
|
|
742
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
743
|
+
React.useEffect(function () {
|
|
744
|
+
let alive = true
|
|
745
|
+
function refresh() {
|
|
746
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
747
|
+
if (!alive) return
|
|
748
|
+
setFIdx((data && data.list) || [])
|
|
749
|
+
const st = data ? data.status : null
|
|
750
|
+
setFOpen(st === 5 || st === 6)
|
|
751
|
+
}).catch(function () { /* 静默失败 */ })
|
|
752
|
+
}
|
|
753
|
+
refresh()
|
|
754
|
+
const stop = ctx.interval(refresh, 60000)
|
|
755
|
+
return function () { alive = false; if (stop) stop() }
|
|
756
|
+
}, [])
|
|
757
|
+
React.useEffect(function () {
|
|
758
|
+
let alive = true
|
|
759
|
+
call('ui.get', {}).then(function (d) {
|
|
760
|
+
if (!alive) return
|
|
761
|
+
if (d && d.mode) ui.set({ mode: d.mode })
|
|
762
|
+
if (d && d.pos) ui.set({ pos: { x: Number(d.pos.x), y: Number(d.pos.y) } })
|
|
763
|
+
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
764
|
+
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
765
|
+
if (d && d.snap) ui.set({ snap: d.snap })
|
|
766
|
+
if (d && (d.snapEdge === 'left' || d.snapEdge === 'right')) ui.set({ snapEdge: d.snapEdge })
|
|
767
|
+
if (d && d.snapW) ui.set({ snapW: Number(d.snapW) })
|
|
768
|
+
if (d && d.tab) ui.set({ tab: d.tab })
|
|
769
|
+
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
770
|
+
}).catch(function () { ui.set({ hydrated: true }) })
|
|
771
|
+
const offUi = ui.subscribe(function () {
|
|
772
|
+
force(function (x) { return x + 1 })
|
|
773
|
+
if (ui.hydrated) saveUi()
|
|
774
|
+
})
|
|
775
|
+
const offBus = bus.on(function (ev) {
|
|
776
|
+
if (ev === 'open') { ui.set({ closed: false, minimized: false, mode: 'floating' }) }
|
|
777
|
+
})
|
|
778
|
+
return function () { alive = false; offUi(); offBus() }
|
|
779
|
+
}, [])
|
|
780
|
+
|
|
781
|
+
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
782
|
+
const s = ui.size
|
|
783
|
+
|
|
784
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
785
|
+
function onTitleDown(e) {
|
|
786
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
787
|
+
if (ui.snapEdge) {
|
|
788
|
+
const vp = viewport()
|
|
789
|
+
if (vp) {
|
|
790
|
+
const x = ui.snapEdge === 'right' ? vp.w - ui.snapW : 0
|
|
791
|
+
ui.set({ snapEdge: null, pos: { x: x, y: 0 }, snap: null })
|
|
792
|
+
} else {
|
|
793
|
+
ui.set({ snapEdge: null, pos: { x: e.clientX - 60, y: e.clientY - 20 }, snap: null })
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
797
|
+
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
798
|
+
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
799
|
+
ui.set({ snap: null })
|
|
800
|
+
setDragTarget(null)
|
|
801
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
802
|
+
}
|
|
803
|
+
function onTitleMove(e) {
|
|
804
|
+
if (!dragRef) return
|
|
805
|
+
ui.set({ pos: { x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) } })
|
|
806
|
+
const vp = viewport()
|
|
807
|
+
if (vp) {
|
|
808
|
+
if (e.clientX >= vp.w - 70) setDragTarget('right')
|
|
809
|
+
else if (e.clientX <= 70) setDragTarget('left')
|
|
810
|
+
else setDragTarget(null)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function onTitleUp(e) {
|
|
814
|
+
dragRef = null
|
|
815
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
816
|
+
if (dragTarget) {
|
|
817
|
+
ui.set({ snapEdge: dragTarget, pos: null, snap: null, snapW: ui.snapW || 380 })
|
|
818
|
+
setDragTarget(null)
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
setDragTarget(null)
|
|
822
|
+
snapCheck()
|
|
823
|
+
}
|
|
824
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
825
|
+
function snapCheck() {
|
|
826
|
+
const vp = viewport()
|
|
827
|
+
const p = ui.pos
|
|
828
|
+
if (!vp || !p) return
|
|
829
|
+
const corners = [
|
|
830
|
+
{ key: 'tl', x: 8, y: 8 },
|
|
831
|
+
{ key: 'tr', x: vp.w - s.w - 8, y: 8 },
|
|
832
|
+
{ key: 'bl', x: 8, y: vp.h - s.h - 8 },
|
|
833
|
+
{ key: 'br', x: vp.w - s.w - 8, y: vp.h - s.h - 8 }
|
|
834
|
+
]
|
|
835
|
+
let best = null, bestD = 110
|
|
836
|
+
for (let i = 0; i < corners.length; i++) {
|
|
837
|
+
const c = corners[i]
|
|
838
|
+
const d = Math.sqrt(Math.pow(p.x - c.x, 2) + Math.pow(p.y - c.y, 2))
|
|
839
|
+
if (d < bestD) { bestD = d; best = c }
|
|
840
|
+
}
|
|
841
|
+
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
842
|
+
}
|
|
843
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
844
|
+
function toggleSnap() {
|
|
845
|
+
const vp = viewport()
|
|
846
|
+
if (ui.snapEdge === 'right') { ui.set({ snapEdge: null }); return }
|
|
847
|
+
if (ui.snap === 'br') {
|
|
848
|
+
ui.set({ snap: null })
|
|
849
|
+
if (vp) ui.set({ snapEdge: 'right', pos: null, snapW: ui.snapW || 380 })
|
|
850
|
+
return
|
|
851
|
+
}
|
|
852
|
+
if (vp) { ui.set({ snap: 'br', pos: { x: Math.max(0, vp.w - s.w - 8), y: Math.max(0, vp.h - s.h - 8) } }) }
|
|
853
|
+
else { ui.set({ snap: 'br', pos: null }) }
|
|
854
|
+
}
|
|
855
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
856
|
+
function onRsDown(e) {
|
|
857
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
858
|
+
resizeRef = ui.snapEdge
|
|
859
|
+
? { w: ui.snapW, h: 0, x: e.clientX, y: e.clientY }
|
|
860
|
+
: { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
|
|
861
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
862
|
+
}
|
|
863
|
+
function onRsMove(e) {
|
|
864
|
+
if (!resizeRef) return
|
|
865
|
+
if (ui.snapEdge) {
|
|
866
|
+
const w = Math.min(Math.max(300, resizeRef.w + (e.clientX - resizeRef.x)), 560)
|
|
867
|
+
ui.set({ snapW: w })
|
|
868
|
+
} else {
|
|
869
|
+
const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
|
|
870
|
+
const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
|
|
871
|
+
ui.set({ size: { w: w, h: h }, snap: null })
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function onRsUp(e) {
|
|
875
|
+
resizeRef = null
|
|
876
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
880
|
+
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
881
|
+
|
|
882
|
+
// 贴靠助手预览(拖动中靠近边缘时显示)
|
|
883
|
+
let snapPreview = null
|
|
884
|
+
if (dragTarget) {
|
|
885
|
+
snapPreview = el('div', {
|
|
886
|
+
key: 'prev', className: 'xq-snap-preview xq-snap-' + dragTarget,
|
|
887
|
+
style: { width: ui.snapW || 380 }
|
|
888
|
+
})
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// 最小化条
|
|
892
|
+
if (ui.minimized) {
|
|
893
|
+
const idxLine = fIdx.map(function (q) {
|
|
894
|
+
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
895
|
+
}).join(' · ')
|
|
896
|
+
let minStyle = null
|
|
897
|
+
if (ui.snapEdge === 'right') minStyle = { right: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
898
|
+
else if (ui.snapEdge === 'left') minStyle = { left: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
899
|
+
else if (ui.pos) minStyle = { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' }
|
|
900
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
901
|
+
el('div', { key: 'min', className: 'xq-float xq-min' + (ui.snapEdge ? ' xq-snapped' : ''), style: minStyle, ...dragProps, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
902
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
903
|
+
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
904
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
905
|
+
el('button', { key: 'x', className: 'xq-btn-mini', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
|
|
906
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
907
|
+
]),
|
|
908
|
+
snapPreview
|
|
909
|
+
])
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
913
|
+
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
914
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
915
|
+
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
916
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
917
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
918
|
+
el('button', { key: 'snap', className: 'xq-btn-mini', title: '循环:右下角 → 右侧窄栏 → 解除', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snapEdge === 'right' ? '◫' : ui.snap === 'br' ? '📌' : '📍'),
|
|
919
|
+
el('button', { key: 'dock', className: 'xq-btn-mini', title: '嵌入到输入框上方(不遮挡对话)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ mode: 'docked', minimized: false }) } }, '嵌入'),
|
|
920
|
+
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
921
|
+
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
922
|
+
])
|
|
923
|
+
|
|
924
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
925
|
+
el('div', { key: 'f', className: 'xq-float' + snapped, style: posStyle() }, [
|
|
926
|
+
titleBar,
|
|
927
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
928
|
+
el(XueqiuPanel, null)
|
|
929
|
+
]),
|
|
930
|
+
el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
|
|
931
|
+
]),
|
|
932
|
+
snapPreview
|
|
933
|
+
])
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function posStyle() {
|
|
937
|
+
if (ui.snapEdge === 'right') return { right: 0, top: 0, bottom: 0, left: 'auto', width: ui.snapW }
|
|
938
|
+
if (ui.snapEdge === 'left') return { left: 0, top: 0, bottom: 0, right: 'auto', width: ui.snapW }
|
|
939
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
940
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
944
|
+
function DockedShell() {
|
|
945
|
+
const [, force] = React.useState(0)
|
|
946
|
+
React.useEffect(function () {
|
|
947
|
+
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
948
|
+
return off
|
|
949
|
+
}, [])
|
|
950
|
+
if (ui.mode !== 'docked' || !ui.hydrated) return null
|
|
951
|
+
|
|
952
|
+
function onDwDown(e) {
|
|
953
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
954
|
+
resizeRef = { w: ui.dockW, h: 0, x: e.clientX, y: e.clientY }
|
|
955
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
956
|
+
}
|
|
957
|
+
function onDwMove(e) {
|
|
958
|
+
if (!resizeRef) return
|
|
959
|
+
ui.set({ dockW: Math.min(Math.max(480, resizeRef.w + (e.clientX - resizeRef.x)), 1100) })
|
|
960
|
+
}
|
|
961
|
+
function onDwUp(e) {
|
|
962
|
+
resizeRef = null
|
|
963
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (ui.minimized) {
|
|
967
|
+
return el('div', { className: 'xq-docked xq-docked-min', style: { width: ui.dockW }, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
968
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
969
|
+
el('span', { key: 'sum', className: 'xq-sum' }, '点击展开行情面板'),
|
|
970
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
971
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
972
|
+
])
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return el('div', { className: 'xq-docked', style: { width: ui.dockW } }, [
|
|
976
|
+
el('div', { key: 'title', className: 'xq-title' }, [
|
|
977
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
978
|
+
el('span', { key: 'hint', className: 'xq-update' }, '已嵌入 · 不遮挡对话'),
|
|
979
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
980
|
+
el('button', { key: 'float', className: 'xq-btn-mini', title: '切换回悬浮模式', onClick: function () { ui.set({ mode: 'floating', minimized: false }) } }, '悬浮'),
|
|
981
|
+
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onClick: function () { ui.set({ minimized: true }) } }, '—')
|
|
982
|
+
]),
|
|
983
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
984
|
+
el(XueqiuPanel, null)
|
|
985
|
+
]),
|
|
986
|
+
el('div', { key: 'rs', className: 'xq-resize xq-resize-w', onPointerDown: onDwDown, onPointerMove: onDwMove, onPointerUp: onDwUp, onPointerCancel: onDwUp })
|
|
987
|
+
])
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function posStyle() {
|
|
991
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
992
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ---------- 输入框上方入口条(仅悬浮模式下显示) ----------
|
|
996
|
+
function Entry() {
|
|
997
|
+
const [indices, setIndices] = React.useState([])
|
|
998
|
+
const [, force] = React.useState(0)
|
|
999
|
+
React.useEffect(function () {
|
|
1000
|
+
let alive = true
|
|
1001
|
+
function refresh() {
|
|
1002
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1003
|
+
if (alive) setIndices((data && data.list) || [])
|
|
1004
|
+
}).catch(function () { /* 静默失败 */ })
|
|
1005
|
+
}
|
|
1006
|
+
refresh()
|
|
1007
|
+
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
1008
|
+
const stop = ctx.interval(refresh, 60000)
|
|
1009
|
+
return function () { alive = false; off(); if (stop) stop() }
|
|
1010
|
+
}, [])
|
|
1011
|
+
if (ui.mode === 'docked') return null
|
|
1012
|
+
const line = indices.map(function (q) {
|
|
1013
|
+
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
1014
|
+
}).join(' · ')
|
|
1015
|
+
return el('div', { className: 'xq-entry', onClick: function () { bus.emit('open') } }, [
|
|
1016
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
1017
|
+
el('span', { key: 'sum', className: 'xq-sum' }, line || '点击打开行情面板'),
|
|
1018
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '打开 ▾')
|
|
824
1019
|
])
|
|
825
1020
|
}
|
|
826
1021
|
|
|
1022
|
+
// ---------- 底部指数条 ----------
|
|
1023
|
+
function Ticker() {
|
|
1024
|
+
const [indices, setIndices] = React.useState([])
|
|
1025
|
+
React.useEffect(function () {
|
|
1026
|
+
let alive = true
|
|
1027
|
+
function refresh() {
|
|
1028
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1029
|
+
if (alive) setIndices((data && data.list) || [])
|
|
1030
|
+
}).catch(function () { /* 静默失败 */ })
|
|
1031
|
+
}
|
|
1032
|
+
refresh()
|
|
1033
|
+
const stop = ctx.interval(refresh, 30000)
|
|
1034
|
+
return function () { alive = false; if (stop) stop() }
|
|
1035
|
+
}, [])
|
|
1036
|
+
if (!indices.length) return null
|
|
1037
|
+
const kids = indices.map(function (q) {
|
|
1038
|
+
return el('span', { key: q.symbol, className: 'xq-tick' }, [
|
|
1039
|
+
el('span', { key: 'n', className: 'xq-tick-name' }, q.name),
|
|
1040
|
+
el('span', { key: 'c', className: 'xq-tick-cur ' + colorOf(q.percent) }, fmt(q.current)),
|
|
1041
|
+
el('span', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent))
|
|
1042
|
+
])
|
|
1043
|
+
})
|
|
1044
|
+
return el('div', { className: 'xq-ticker', onClick: function () { bus.emit('open') } }, kids)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 追加样式:嵌入外壳 / 缩放手柄 / 磁吸动画
|
|
1048
|
+
styles.insert(
|
|
1049
|
+
'.xq-docked{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;position:relative;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary);margin-bottom:2px;}\n' +
|
|
1050
|
+
'.xq-docked *{box-sizing:border-box;}\n' +
|
|
1051
|
+
'.xq-docked-min{display:flex;align-items:center;gap:10px;cursor:pointer;padding:6px 12px;}\n' +
|
|
1052
|
+
'.xq-docked .xq-title{cursor:default;}\n' +
|
|
1053
|
+
'.xq-resize{position:absolute;right:2px;bottom:2px;width:18px;height:18px;cursor:nwse-resize;z-index:6;}\n' +
|
|
1054
|
+
'.xq-resize::after{content:"";position:absolute;right:4px;bottom:4px;width:8px;height:8px;border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);opacity:.55;}\n' +
|
|
1055
|
+
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
1056
|
+
'.xq-resize-w::after{display:none;}\n' +
|
|
1057
|
+
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n' +
|
|
1058
|
+
'.xq-wrap{pointer-events:none;position:fixed;inset:0;z-index:1200;}\n' +
|
|
1059
|
+
'.xq-wrap .xq-float{pointer-events:auto;}\n' +
|
|
1060
|
+
'.xq-snap-preview{position:fixed;top:8px;bottom:8px;border:2px solid var(--dsw-alias-brand-primary);border-radius:10px;background:var(--dsw-alias-brand-primary);opacity:.13;pointer-events:none;z-index:1199;}\n' +
|
|
1061
|
+
'.xq-snap-right{right:8px;}\n' +
|
|
1062
|
+
'.xq-snap-left{left:8px;}\n' +
|
|
1063
|
+
'.xq-float.xq-narrow .xq-grid-hd,.xq-float.xq-narrow .xq-grid-row{grid-template-columns:1.5fr 1fr 1fr 62px;}\n' +
|
|
1064
|
+
'.xq-float.xq-narrow .xq-vol-col{display:none;}\n' +
|
|
1065
|
+
'.xq-float.xq-narrow .xq-idx-card{min-width:88px;padding:5px 6px;}\n' +
|
|
1066
|
+
'.xq-float.xq-narrow .xq-idx-cur{font-size:12px;}\n' +
|
|
1067
|
+
'.xq-float.xq-narrow .xq-stats{grid-template-columns:repeat(2,1fr);}\n' +
|
|
1068
|
+
'.xq-float.xq-narrow .xq-detail-cur{font-size:16px;}\n' +
|
|
1069
|
+
'.xq-float.xq-narrow .xq-periods button,.xq-float.xq-narrow .xq-modes button{padding:2px 6px;}\n'
|
|
1070
|
+
)
|
|
1071
|
+
|
|
827
1072
|
// ---------- 注册 ----------
|
|
828
1073
|
const slots = ctx.get('slots')
|
|
829
1074
|
if (slots === undefined) return
|
|
830
1075
|
slots.inject('shell.overlay', function () {
|
|
831
1076
|
return slots.register(
|
|
832
1077
|
{ name: 'shell.overlay', id: 'xueqiu-panel' },
|
|
833
|
-
function () { return el(
|
|
1078
|
+
function () { return el(FloatingShell, null) }
|
|
834
1079
|
)
|
|
835
1080
|
})
|
|
836
1081
|
slots.inject('conversation.input.dock', function () {
|
|
@@ -839,11 +1084,18 @@ return {
|
|
|
839
1084
|
function () { return el(Entry, null) }
|
|
840
1085
|
)
|
|
841
1086
|
})
|
|
1087
|
+
slots.inject('conversation.input.dock', function () {
|
|
1088
|
+
return slots.register(
|
|
1089
|
+
{ name: 'conversation.input.dock', id: 'xueqiu-docked', order: 31 },
|
|
1090
|
+
function () { return el(DockedShell, null) }
|
|
1091
|
+
)
|
|
1092
|
+
})
|
|
842
1093
|
slots.inject('conversation.composer.dock', function () {
|
|
843
1094
|
return slots.register(
|
|
844
1095
|
{ name: 'conversation.composer.dock', id: 'xueqiu-ticker', order: 1 },
|
|
845
1096
|
function () { return el(Ticker, null) }
|
|
846
1097
|
)
|
|
847
1098
|
})
|
|
1099
|
+
|
|
848
1100
|
}
|
|
849
|
-
}
|
|
1101
|
+
}
|
package/dynamic/host.js
CHANGED
|
@@ -385,7 +385,13 @@ return {
|
|
|
385
385
|
|
|
386
386
|
async function actUiGet() {
|
|
387
387
|
const s = await loadUiState()
|
|
388
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
mode: s.mode || 'floating', pos: s.pos || null,
|
|
390
|
+
size: (s.size && isFinite(Number(s.size.w)) && isFinite(Number(s.size.h))) ? { w: Number(s.size.w), h: Number(s.size.h) } : null,
|
|
391
|
+
dockW: s.dockW || null, snap: s.snap || null,
|
|
392
|
+
snapEdge: s.snapEdge || null, snapW: s.snapW || 380,
|
|
393
|
+
tab: s.tab || 'market', minimized: !!s.minimized
|
|
394
|
+
}
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
async function actUiSet(args) {
|
|
@@ -395,6 +401,14 @@ return {
|
|
|
395
401
|
}
|
|
396
402
|
if (args.tab !== undefined) s.tab = String(args.tab)
|
|
397
403
|
if (args.minimized !== undefined) s.minimized = !!args.minimized
|
|
404
|
+
if (args.mode === 'floating' || args.mode === 'docked') s.mode = args.mode
|
|
405
|
+
if (args.size && typeof args.size === 'object' && isFinite(Number(args.size.w)) && isFinite(Number(args.size.h))) {
|
|
406
|
+
s.size = { w: Number(args.size.w), h: Number(args.size.h) }
|
|
407
|
+
}
|
|
408
|
+
if (args.dockW !== undefined && isFinite(Number(args.dockW))) s.dockW = Number(args.dockW)
|
|
409
|
+
if ('snap' in args && args.snap !== undefined) s.snap = args.snap === null ? null : String(args.snap)
|
|
410
|
+
if (args.snapEdge === 'left' || args.snapEdge === 'right' || args.snapEdge === null) s.snapEdge = args.snapEdge
|
|
411
|
+
if (args.snapW !== undefined && isFinite(Number(args.snapW))) s.snapW = Number(args.snapW)
|
|
398
412
|
await saveUiState()
|
|
399
413
|
return { ok: true }
|
|
400
414
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-xueqiu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "雪球 mini 行情面板 — DeepSeek Harness 免登录 A股/港美股实时行情、K线(蜡烛/成交量/均线)、分时图、热榜、搜索、7x24 快讯、热议用户。可拖拽悬浮面板,交易时段智能刷新。",
|
|
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
|
@@ -174,13 +174,7 @@ export default {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
178
|
-
const saveUi = ctx.debounce(function (s) {
|
|
179
|
-
call('ui.set', s).catch(function () { /* 忽略 */ })
|
|
180
|
-
}, 800)
|
|
181
|
-
|
|
182
|
-
// 拖拽用可变引用(面板实例唯一)
|
|
183
|
-
let dragRef = null
|
|
177
|
+
// (saveUi / dragRef / ui 共享状态见下方新块)
|
|
184
178
|
|
|
185
179
|
// ---------- K线蜡烛图(含成交量与均线) ----------
|
|
186
180
|
function KlineChart(props) {
|
|
@@ -314,61 +308,54 @@ export default {
|
|
|
314
308
|
])
|
|
315
309
|
}
|
|
316
310
|
|
|
317
|
-
// ----------
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
311
|
+
// ---------- 共享 UI 状态(悬浮/嵌入/位置/尺寸/标签页) ----------
|
|
312
|
+
const ui = {
|
|
313
|
+
mode: 'floating', pos: null, size: { w: 720, h: 640 }, dockW: 720,
|
|
314
|
+
snap: null, snapEdge: null, snapW: 380,
|
|
315
|
+
minimized: false, tab: 'market', closed: false, hydrated: false,
|
|
316
|
+
_fns: [],
|
|
317
|
+
subscribe: function (fn) {
|
|
318
|
+
this._fns.push(fn)
|
|
319
|
+
const self = this
|
|
320
|
+
return function () {
|
|
321
|
+
const i = self._fns.indexOf(fn)
|
|
322
|
+
if (i !== -1) self._fns.splice(i, 1)
|
|
326
323
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
return el('div', { className: 'xq-ticker', onClick: function () { bus.emit('open') } }, kids)
|
|
324
|
+
},
|
|
325
|
+
notify: function () {
|
|
326
|
+
const fns = this._fns.slice()
|
|
327
|
+
for (let i = 0; i < fns.length; i++) { try { fns[i]() } catch (e) { /* ignore */ } }
|
|
328
|
+
},
|
|
329
|
+
set: function (patch) {
|
|
330
|
+
let ch = false
|
|
331
|
+
for (const k in patch) {
|
|
332
|
+
if (this[k] !== patch[k]) { this[k] = patch[k]; ch = true }
|
|
333
|
+
}
|
|
334
|
+
if (ch) this.notify()
|
|
335
|
+
}
|
|
340
336
|
}
|
|
341
337
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
let alive = true
|
|
347
|
-
function refresh() {
|
|
348
|
-
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
349
|
-
if (alive) setIndices((data && data.list) || [])
|
|
350
|
-
}).catch(function () { /* 静默失败 */ })
|
|
351
|
-
}
|
|
352
|
-
refresh()
|
|
353
|
-
const stop = ctx.interval(refresh, 60000)
|
|
354
|
-
return function () { alive = false; if (stop) stop() }
|
|
355
|
-
}, [])
|
|
356
|
-
const line = indices.map(function (q) {
|
|
357
|
-
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
358
|
-
}).join(' · ')
|
|
359
|
-
return el('div', { className: 'xq-entry', onClick: function () { bus.emit('open') } }, [
|
|
360
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
361
|
-
el('span', { key: 'sum', className: 'xq-sum' }, line || '点击打开行情面板'),
|
|
362
|
-
el('span', { key: 'caret', className: 'xq-caret' }, '打开 ▾')
|
|
363
|
-
])
|
|
338
|
+
function viewport() {
|
|
339
|
+
try { if (typeof window !== 'undefined' && window.innerWidth) return { w: window.innerWidth, h: window.innerHeight } } catch (e) { /* ignore */ }
|
|
340
|
+
try { if (typeof document !== 'undefined' && document.documentElement && document.documentElement.clientWidth) return { w: document.documentElement.clientWidth, h: document.documentElement.clientHeight } } catch (e) { /* ignore */ }
|
|
341
|
+
return null
|
|
364
342
|
}
|
|
365
343
|
|
|
366
|
-
//
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
344
|
+
// 保存 UI 状态(防抖)
|
|
345
|
+
const saveUi = ctx.debounce(function () {
|
|
346
|
+
call('ui.set', {
|
|
347
|
+
mode: ui.mode, pos: ui.pos, size: ui.size, dockW: ui.dockW,
|
|
348
|
+
snap: ui.snap, snapEdge: ui.snapEdge, snapW: ui.snapW,
|
|
349
|
+
tab: ui.tab, minimized: ui.minimized
|
|
350
|
+
}).catch(function () { /* 忽略 */ })
|
|
351
|
+
}, 800)
|
|
352
|
+
|
|
353
|
+
// 拖拽 / 缩放用可变引用(实例唯一)
|
|
354
|
+
let dragRef = null
|
|
355
|
+
let resizeRef = null
|
|
356
|
+
|
|
357
|
+
// ---------- 面板内容(数据 + tabs + 详情),由悬浮/嵌入外壳共用 ----------
|
|
358
|
+
function XueqiuPanel() {
|
|
372
359
|
const [watchlist, setWatchlist] = React.useState([])
|
|
373
360
|
const [quotes, setQuotes] = React.useState([])
|
|
374
361
|
const [indices, setIndices] = React.useState([])
|
|
@@ -388,30 +375,8 @@ export default {
|
|
|
388
375
|
const [updatedAt, setUpdatedAt] = React.useState(null)
|
|
389
376
|
const [err, setErr] = React.useState('')
|
|
390
377
|
const [loading, setLoading] = React.useState(true)
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
// 恢复 UI 状态 + 监听 bus
|
|
394
|
-
React.useEffect(function () {
|
|
395
|
-
let alive = true
|
|
396
|
-
call('ui.get', {}).then(function (d) {
|
|
397
|
-
if (!alive) return
|
|
398
|
-
if (d && d.pos) setPos({ x: Number(d.pos.x), y: Number(d.pos.y) })
|
|
399
|
-
if (d && d.tab) setTab(d.tab)
|
|
400
|
-
setMinimized(!!(d && d.minimized))
|
|
401
|
-
setHydrated(true)
|
|
402
|
-
}).catch(function () { setHydrated(true) })
|
|
403
|
-
const off = bus.on(function (ev) {
|
|
404
|
-
if (ev === 'open') { setVisible(true); setMinimized(false) }
|
|
405
|
-
if (ev === 'close') setVisible(false)
|
|
406
|
-
})
|
|
407
|
-
return function () { alive = false; off() }
|
|
408
|
-
}, [])
|
|
409
|
-
|
|
410
|
-
// 持久化 UI 状态(hydrated 后再写,避免覆盖)
|
|
411
|
-
React.useEffect(function () {
|
|
412
|
-
if (!hydrated) return
|
|
413
|
-
saveUi({ tab: tab, minimized: minimized, pos: pos })
|
|
414
|
-
}, [tab, minimized, pos, hydrated])
|
|
378
|
+
const tab = ui.tab
|
|
379
|
+
function setTab(t) { ui.set({ tab: t }) }
|
|
415
380
|
|
|
416
381
|
function refreshMarket() {
|
|
417
382
|
return Promise.all([
|
|
@@ -527,58 +492,6 @@ export default {
|
|
|
527
492
|
setManualCode('')
|
|
528
493
|
}
|
|
529
494
|
|
|
530
|
-
// 拖拽
|
|
531
|
-
function onTitleDown(e) {
|
|
532
|
-
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
533
|
-
const baseX = pos ? pos.x : e.clientX
|
|
534
|
-
const baseY = pos ? pos.y : e.clientY
|
|
535
|
-
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
536
|
-
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
537
|
-
}
|
|
538
|
-
function onTitleMove(e) {
|
|
539
|
-
if (!dragRef) return
|
|
540
|
-
setPos({ x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) })
|
|
541
|
-
}
|
|
542
|
-
function onTitleUp(e) {
|
|
543
|
-
dragRef = null
|
|
544
|
-
try {
|
|
545
|
-
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId)
|
|
546
|
-
} catch (err) { /* ignore */ }
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (!visible) return null
|
|
550
|
-
|
|
551
|
-
const dragProps = {
|
|
552
|
-
onPointerDown: onTitleDown,
|
|
553
|
-
onPointerMove: onTitleMove,
|
|
554
|
-
onPointerUp: onTitleUp,
|
|
555
|
-
onPointerCancel: onTitleUp
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// 最小化条
|
|
559
|
-
if (minimized) {
|
|
560
|
-
const idxLine = indices.map(function (q) {
|
|
561
|
-
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
562
|
-
}).join(' · ')
|
|
563
|
-
return el('div', { className: 'xq-float xq-min', style: pos ? { left: pos.x, top: pos.y, right: 'auto', bottom: 'auto' } : null, onClick: function () { setMinimized(false) }, ...dragProps }, [
|
|
564
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
565
|
-
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
566
|
-
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
567
|
-
el('button', { key: 'x', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); setVisible(false) }, onPointerDown: function (e) { e.stopPropagation() } }, '✕'),
|
|
568
|
-
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
569
|
-
])
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// 标题栏
|
|
573
|
-
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
574
|
-
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
575
|
-
el('span', { key: 'st', className: 'xq-status' + (marketOpen ? ' xq-live' : '') }, marketOpen ? '● 盘中' : '已收盘'),
|
|
576
|
-
el('span', { key: 'upd', className: 'xq-update' }, updatedAt ? ('更新 ' + fmtTime(updatedAt)) : (loading ? '加载中…' : '')),
|
|
577
|
-
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
578
|
-
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); setMinimized(true) } }, '—'),
|
|
579
|
-
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(可从下方入口重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); setVisible(false) } }, '✕')
|
|
580
|
-
])
|
|
581
|
-
|
|
582
495
|
const tabs = el('div', { className: 'xq-tabs' }, [
|
|
583
496
|
['market', '行情'], ['hot', '热榜'], ['search', '搜索'], ['news', '快讯']
|
|
584
497
|
].map(function (t) {
|
|
@@ -618,7 +531,7 @@ export default {
|
|
|
618
531
|
]),
|
|
619
532
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
620
533
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
621
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
534
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
622
535
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
623
536
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
624
537
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -823,19 +736,351 @@ export default {
|
|
|
823
736
|
content = NewsTab()
|
|
824
737
|
}
|
|
825
738
|
|
|
826
|
-
return el('div',
|
|
827
|
-
|
|
828
|
-
|
|
739
|
+
return el('div', null, [tabs, errBox, content])
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
743
|
+
function FloatingShell() {
|
|
744
|
+
const [, force] = React.useState(0)
|
|
745
|
+
const [fIdx, setFIdx] = React.useState([])
|
|
746
|
+
const [fOpen, setFOpen] = React.useState(true)
|
|
747
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
748
|
+
React.useEffect(function () {
|
|
749
|
+
let alive = true
|
|
750
|
+
function refresh() {
|
|
751
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
752
|
+
if (!alive) return
|
|
753
|
+
setFIdx((data && data.list) || [])
|
|
754
|
+
const st = data ? data.status : null
|
|
755
|
+
setFOpen(st === 5 || st === 6)
|
|
756
|
+
}).catch(function () { /* 静默失败 */ })
|
|
757
|
+
}
|
|
758
|
+
refresh()
|
|
759
|
+
const stop = ctx.interval(refresh, 60000)
|
|
760
|
+
return function () { alive = false; if (stop) stop() }
|
|
761
|
+
}, [])
|
|
762
|
+
React.useEffect(function () {
|
|
763
|
+
let alive = true
|
|
764
|
+
call('ui.get', {}).then(function (d) {
|
|
765
|
+
if (!alive) return
|
|
766
|
+
if (d && d.mode) ui.set({ mode: d.mode })
|
|
767
|
+
if (d && d.pos) ui.set({ pos: { x: Number(d.pos.x), y: Number(d.pos.y) } })
|
|
768
|
+
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
769
|
+
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
770
|
+
if (d && d.snap) ui.set({ snap: d.snap })
|
|
771
|
+
if (d && (d.snapEdge === 'left' || d.snapEdge === 'right')) ui.set({ snapEdge: d.snapEdge })
|
|
772
|
+
if (d && d.snapW) ui.set({ snapW: Number(d.snapW) })
|
|
773
|
+
if (d && d.tab) ui.set({ tab: d.tab })
|
|
774
|
+
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
775
|
+
}).catch(function () { ui.set({ hydrated: true }) })
|
|
776
|
+
const offUi = ui.subscribe(function () {
|
|
777
|
+
force(function (x) { return x + 1 })
|
|
778
|
+
if (ui.hydrated) saveUi()
|
|
779
|
+
})
|
|
780
|
+
const offBus = bus.on(function (ev) {
|
|
781
|
+
if (ev === 'open') { ui.set({ closed: false, minimized: false, mode: 'floating' }) }
|
|
782
|
+
})
|
|
783
|
+
return function () { alive = false; offUi(); offBus() }
|
|
784
|
+
}, [])
|
|
785
|
+
|
|
786
|
+
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
787
|
+
const s = ui.size
|
|
788
|
+
|
|
789
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
790
|
+
function onTitleDown(e) {
|
|
791
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
792
|
+
if (ui.snapEdge) {
|
|
793
|
+
const vp = viewport()
|
|
794
|
+
if (vp) {
|
|
795
|
+
const x = ui.snapEdge === 'right' ? vp.w - ui.snapW : 0
|
|
796
|
+
ui.set({ snapEdge: null, pos: { x: x, y: 0 }, snap: null })
|
|
797
|
+
} else {
|
|
798
|
+
ui.set({ snapEdge: null, pos: { x: e.clientX - 60, y: e.clientY - 20 }, snap: null })
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
802
|
+
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
803
|
+
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
804
|
+
ui.set({ snap: null })
|
|
805
|
+
setDragTarget(null)
|
|
806
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
807
|
+
}
|
|
808
|
+
function onTitleMove(e) {
|
|
809
|
+
if (!dragRef) return
|
|
810
|
+
ui.set({ pos: { x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) } })
|
|
811
|
+
const vp = viewport()
|
|
812
|
+
if (vp) {
|
|
813
|
+
if (e.clientX >= vp.w - 70) setDragTarget('right')
|
|
814
|
+
else if (e.clientX <= 70) setDragTarget('left')
|
|
815
|
+
else setDragTarget(null)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function onTitleUp(e) {
|
|
819
|
+
dragRef = null
|
|
820
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
821
|
+
if (dragTarget) {
|
|
822
|
+
ui.set({ snapEdge: dragTarget, pos: null, snap: null, snapW: ui.snapW || 380 })
|
|
823
|
+
setDragTarget(null)
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
setDragTarget(null)
|
|
827
|
+
snapCheck()
|
|
828
|
+
}
|
|
829
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
830
|
+
function snapCheck() {
|
|
831
|
+
const vp = viewport()
|
|
832
|
+
const p = ui.pos
|
|
833
|
+
if (!vp || !p) return
|
|
834
|
+
const corners = [
|
|
835
|
+
{ key: 'tl', x: 8, y: 8 },
|
|
836
|
+
{ key: 'tr', x: vp.w - s.w - 8, y: 8 },
|
|
837
|
+
{ key: 'bl', x: 8, y: vp.h - s.h - 8 },
|
|
838
|
+
{ key: 'br', x: vp.w - s.w - 8, y: vp.h - s.h - 8 }
|
|
839
|
+
]
|
|
840
|
+
let best = null, bestD = 110
|
|
841
|
+
for (let i = 0; i < corners.length; i++) {
|
|
842
|
+
const c = corners[i]
|
|
843
|
+
const d = Math.sqrt(Math.pow(p.x - c.x, 2) + Math.pow(p.y - c.y, 2))
|
|
844
|
+
if (d < bestD) { bestD = d; best = c }
|
|
845
|
+
}
|
|
846
|
+
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
847
|
+
}
|
|
848
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
849
|
+
function toggleSnap() {
|
|
850
|
+
const vp = viewport()
|
|
851
|
+
if (ui.snapEdge === 'right') { ui.set({ snapEdge: null }); return }
|
|
852
|
+
if (ui.snap === 'br') {
|
|
853
|
+
ui.set({ snap: null })
|
|
854
|
+
if (vp) ui.set({ snapEdge: 'right', pos: null, snapW: ui.snapW || 380 })
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
if (vp) { ui.set({ snap: 'br', pos: { x: Math.max(0, vp.w - s.w - 8), y: Math.max(0, vp.h - s.h - 8) } }) }
|
|
858
|
+
else { ui.set({ snap: 'br', pos: null }) }
|
|
859
|
+
}
|
|
860
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
861
|
+
function onRsDown(e) {
|
|
862
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
863
|
+
resizeRef = ui.snapEdge
|
|
864
|
+
? { w: ui.snapW, h: 0, x: e.clientX, y: e.clientY }
|
|
865
|
+
: { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
|
|
866
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
867
|
+
}
|
|
868
|
+
function onRsMove(e) {
|
|
869
|
+
if (!resizeRef) return
|
|
870
|
+
if (ui.snapEdge) {
|
|
871
|
+
const w = Math.min(Math.max(300, resizeRef.w + (e.clientX - resizeRef.x)), 560)
|
|
872
|
+
ui.set({ snapW: w })
|
|
873
|
+
} else {
|
|
874
|
+
const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
|
|
875
|
+
const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
|
|
876
|
+
ui.set({ size: { w: w, h: h }, snap: null })
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function onRsUp(e) {
|
|
880
|
+
resizeRef = null
|
|
881
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
885
|
+
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
886
|
+
|
|
887
|
+
// 贴靠助手预览(拖动中靠近边缘时显示)
|
|
888
|
+
let snapPreview = null
|
|
889
|
+
if (dragTarget) {
|
|
890
|
+
snapPreview = el('div', {
|
|
891
|
+
key: 'prev', className: 'xq-snap-preview xq-snap-' + dragTarget,
|
|
892
|
+
style: { width: ui.snapW || 380 }
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 最小化条
|
|
897
|
+
if (ui.minimized) {
|
|
898
|
+
const idxLine = fIdx.map(function (q) {
|
|
899
|
+
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
900
|
+
}).join(' · ')
|
|
901
|
+
let minStyle = null
|
|
902
|
+
if (ui.snapEdge === 'right') minStyle = { right: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
903
|
+
else if (ui.snapEdge === 'left') minStyle = { left: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
904
|
+
else if (ui.pos) minStyle = { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' }
|
|
905
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
906
|
+
el('div', { key: 'min', className: 'xq-float xq-min' + (ui.snapEdge ? ' xq-snapped' : ''), style: minStyle, ...dragProps, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
907
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
908
|
+
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
909
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
910
|
+
el('button', { key: 'x', className: 'xq-btn-mini', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
|
|
911
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
912
|
+
]),
|
|
913
|
+
snapPreview
|
|
914
|
+
])
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
918
|
+
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
919
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
920
|
+
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
921
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
922
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
923
|
+
el('button', { key: 'snap', className: 'xq-btn-mini', title: '循环:右下角 → 右侧窄栏 → 解除', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snapEdge === 'right' ? '◫' : ui.snap === 'br' ? '📌' : '📍'),
|
|
924
|
+
el('button', { key: 'dock', className: 'xq-btn-mini', title: '嵌入到输入框上方(不遮挡对话)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ mode: 'docked', minimized: false }) } }, '嵌入'),
|
|
925
|
+
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
926
|
+
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
927
|
+
])
|
|
928
|
+
|
|
929
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
930
|
+
el('div', { key: 'f', className: 'xq-float' + snapped, style: posStyle() }, [
|
|
931
|
+
titleBar,
|
|
932
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
933
|
+
el(XueqiuPanel, null)
|
|
934
|
+
]),
|
|
935
|
+
el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
|
|
936
|
+
]),
|
|
937
|
+
snapPreview
|
|
938
|
+
])
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function posStyle() {
|
|
942
|
+
if (ui.snapEdge === 'right') return { right: 0, top: 0, bottom: 0, left: 'auto', width: ui.snapW }
|
|
943
|
+
if (ui.snapEdge === 'left') return { left: 0, top: 0, bottom: 0, right: 'auto', width: ui.snapW }
|
|
944
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
945
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
949
|
+
function DockedShell() {
|
|
950
|
+
const [, force] = React.useState(0)
|
|
951
|
+
React.useEffect(function () {
|
|
952
|
+
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
953
|
+
return off
|
|
954
|
+
}, [])
|
|
955
|
+
if (ui.mode !== 'docked' || !ui.hydrated) return null
|
|
956
|
+
|
|
957
|
+
function onDwDown(e) {
|
|
958
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
959
|
+
resizeRef = { w: ui.dockW, h: 0, x: e.clientX, y: e.clientY }
|
|
960
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
961
|
+
}
|
|
962
|
+
function onDwMove(e) {
|
|
963
|
+
if (!resizeRef) return
|
|
964
|
+
ui.set({ dockW: Math.min(Math.max(480, resizeRef.w + (e.clientX - resizeRef.x)), 1100) })
|
|
965
|
+
}
|
|
966
|
+
function onDwUp(e) {
|
|
967
|
+
resizeRef = null
|
|
968
|
+
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (ui.minimized) {
|
|
972
|
+
return el('div', { className: 'xq-docked xq-docked-min', style: { width: ui.dockW }, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
973
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
974
|
+
el('span', { key: 'sum', className: 'xq-sum' }, '点击展开行情面板'),
|
|
975
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
976
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
977
|
+
])
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return el('div', { className: 'xq-docked', style: { width: ui.dockW } }, [
|
|
981
|
+
el('div', { key: 'title', className: 'xq-title' }, [
|
|
982
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
983
|
+
el('span', { key: 'hint', className: 'xq-update' }, '已嵌入 · 不遮挡对话'),
|
|
984
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
985
|
+
el('button', { key: 'float', className: 'xq-btn-mini', title: '切换回悬浮模式', onClick: function () { ui.set({ mode: 'floating', minimized: false }) } }, '悬浮'),
|
|
986
|
+
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onClick: function () { ui.set({ minimized: true }) } }, '—')
|
|
987
|
+
]),
|
|
988
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
989
|
+
el(XueqiuPanel, null)
|
|
990
|
+
]),
|
|
991
|
+
el('div', { key: 'rs', className: 'xq-resize xq-resize-w', onPointerDown: onDwDown, onPointerMove: onDwMove, onPointerUp: onDwUp, onPointerCancel: onDwUp })
|
|
992
|
+
])
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function posStyle() {
|
|
996
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
997
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ---------- 输入框上方入口条(仅悬浮模式下显示) ----------
|
|
1001
|
+
function Entry() {
|
|
1002
|
+
const [indices, setIndices] = React.useState([])
|
|
1003
|
+
const [, force] = React.useState(0)
|
|
1004
|
+
React.useEffect(function () {
|
|
1005
|
+
let alive = true
|
|
1006
|
+
function refresh() {
|
|
1007
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1008
|
+
if (alive) setIndices((data && data.list) || [])
|
|
1009
|
+
}).catch(function () { /* 静默失败 */ })
|
|
1010
|
+
}
|
|
1011
|
+
refresh()
|
|
1012
|
+
const off = ui.subscribe(function () { force(function (x) { return x + 1 }) })
|
|
1013
|
+
const stop = ctx.interval(refresh, 60000)
|
|
1014
|
+
return function () { alive = false; off(); if (stop) stop() }
|
|
1015
|
+
}, [])
|
|
1016
|
+
if (ui.mode === 'docked') return null
|
|
1017
|
+
const line = indices.map(function (q) {
|
|
1018
|
+
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
1019
|
+
}).join(' · ')
|
|
1020
|
+
return el('div', { className: 'xq-entry', onClick: function () { bus.emit('open') } }, [
|
|
1021
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
1022
|
+
el('span', { key: 'sum', className: 'xq-sum' }, line || '点击打开行情面板'),
|
|
1023
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '打开 ▾')
|
|
829
1024
|
])
|
|
830
1025
|
}
|
|
831
1026
|
|
|
1027
|
+
// ---------- 底部指数条 ----------
|
|
1028
|
+
function Ticker() {
|
|
1029
|
+
const [indices, setIndices] = React.useState([])
|
|
1030
|
+
React.useEffect(function () {
|
|
1031
|
+
let alive = true
|
|
1032
|
+
function refresh() {
|
|
1033
|
+
call('quote', { symbols: ['SH000001', 'SZ399001', 'SZ399006', 'SH000300'] }).then(function (data) {
|
|
1034
|
+
if (alive) setIndices((data && data.list) || [])
|
|
1035
|
+
}).catch(function () { /* 静默失败 */ })
|
|
1036
|
+
}
|
|
1037
|
+
refresh()
|
|
1038
|
+
const stop = ctx.interval(refresh, 30000)
|
|
1039
|
+
return function () { alive = false; if (stop) stop() }
|
|
1040
|
+
}, [])
|
|
1041
|
+
if (!indices.length) return null
|
|
1042
|
+
const kids = indices.map(function (q) {
|
|
1043
|
+
return el('span', { key: q.symbol, className: 'xq-tick' }, [
|
|
1044
|
+
el('span', { key: 'n', className: 'xq-tick-name' }, q.name),
|
|
1045
|
+
el('span', { key: 'c', className: 'xq-tick-cur ' + colorOf(q.percent) }, fmt(q.current)),
|
|
1046
|
+
el('span', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent))
|
|
1047
|
+
])
|
|
1048
|
+
})
|
|
1049
|
+
return el('div', { className: 'xq-ticker', onClick: function () { bus.emit('open') } }, kids)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// 追加样式:嵌入外壳 / 缩放手柄 / 磁吸动画
|
|
1053
|
+
styles.insert(
|
|
1054
|
+
'.xq-docked{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;position:relative;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary);margin-bottom:2px;}\n' +
|
|
1055
|
+
'.xq-docked *{box-sizing:border-box;}\n' +
|
|
1056
|
+
'.xq-docked-min{display:flex;align-items:center;gap:10px;cursor:pointer;padding:6px 12px;}\n' +
|
|
1057
|
+
'.xq-docked .xq-title{cursor:default;}\n' +
|
|
1058
|
+
'.xq-resize{position:absolute;right:2px;bottom:2px;width:18px;height:18px;cursor:nwse-resize;z-index:6;}\n' +
|
|
1059
|
+
'.xq-resize::after{content:"";position:absolute;right:4px;bottom:4px;width:8px;height:8px;border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);opacity:.55;}\n' +
|
|
1060
|
+
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
1061
|
+
'.xq-resize-w::after{display:none;}\n' +
|
|
1062
|
+
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n' +
|
|
1063
|
+
'.xq-wrap{pointer-events:none;position:fixed;inset:0;z-index:1200;}\n' +
|
|
1064
|
+
'.xq-wrap .xq-float{pointer-events:auto;}\n' +
|
|
1065
|
+
'.xq-snap-preview{position:fixed;top:8px;bottom:8px;border:2px solid var(--dsw-alias-brand-primary);border-radius:10px;background:var(--dsw-alias-brand-primary);opacity:.13;pointer-events:none;z-index:1199;}\n' +
|
|
1066
|
+
'.xq-snap-right{right:8px;}\n' +
|
|
1067
|
+
'.xq-snap-left{left:8px;}\n' +
|
|
1068
|
+
'.xq-float.xq-narrow .xq-grid-hd,.xq-float.xq-narrow .xq-grid-row{grid-template-columns:1.5fr 1fr 1fr 62px;}\n' +
|
|
1069
|
+
'.xq-float.xq-narrow .xq-vol-col{display:none;}\n' +
|
|
1070
|
+
'.xq-float.xq-narrow .xq-idx-card{min-width:88px;padding:5px 6px;}\n' +
|
|
1071
|
+
'.xq-float.xq-narrow .xq-idx-cur{font-size:12px;}\n' +
|
|
1072
|
+
'.xq-float.xq-narrow .xq-stats{grid-template-columns:repeat(2,1fr);}\n' +
|
|
1073
|
+
'.xq-float.xq-narrow .xq-detail-cur{font-size:16px;}\n' +
|
|
1074
|
+
'.xq-float.xq-narrow .xq-periods button,.xq-float.xq-narrow .xq-modes button{padding:2px 6px;}\n'
|
|
1075
|
+
)
|
|
1076
|
+
|
|
832
1077
|
// ---------- 注册 ----------
|
|
833
1078
|
const slots = ctx.get('slots')
|
|
834
1079
|
if (slots === undefined) return
|
|
835
1080
|
slots.inject('shell.overlay', function () {
|
|
836
1081
|
return slots.register(
|
|
837
1082
|
{ name: 'shell.overlay', id: 'xueqiu-panel' },
|
|
838
|
-
function () { return el(
|
|
1083
|
+
function () { return el(FloatingShell, null) }
|
|
839
1084
|
)
|
|
840
1085
|
})
|
|
841
1086
|
slots.inject('conversation.input.dock', function () {
|
|
@@ -844,11 +1089,18 @@ export default {
|
|
|
844
1089
|
function () { return el(Entry, null) }
|
|
845
1090
|
)
|
|
846
1091
|
})
|
|
1092
|
+
slots.inject('conversation.input.dock', function () {
|
|
1093
|
+
return slots.register(
|
|
1094
|
+
{ name: 'conversation.input.dock', id: 'xueqiu-docked', order: 31 },
|
|
1095
|
+
function () { return el(DockedShell, null) }
|
|
1096
|
+
)
|
|
1097
|
+
})
|
|
847
1098
|
slots.inject('conversation.composer.dock', function () {
|
|
848
1099
|
return slots.register(
|
|
849
1100
|
{ name: 'conversation.composer.dock', id: 'xueqiu-ticker', order: 1 },
|
|
850
1101
|
function () { return el(Ticker, null) }
|
|
851
1102
|
)
|
|
852
1103
|
})
|
|
1104
|
+
|
|
853
1105
|
}
|
|
854
|
-
}
|
|
1106
|
+
}
|
package/src/index.js
CHANGED
|
@@ -386,7 +386,13 @@ export default {
|
|
|
386
386
|
|
|
387
387
|
async function actUiGet() {
|
|
388
388
|
const s = await loadUiState()
|
|
389
|
-
return {
|
|
389
|
+
return {
|
|
390
|
+
mode: s.mode || 'floating', pos: s.pos || null,
|
|
391
|
+
size: (s.size && isFinite(Number(s.size.w)) && isFinite(Number(s.size.h))) ? { w: Number(s.size.w), h: Number(s.size.h) } : null,
|
|
392
|
+
dockW: s.dockW || null, snap: s.snap || null,
|
|
393
|
+
snapEdge: s.snapEdge || null, snapW: s.snapW || 380,
|
|
394
|
+
tab: s.tab || 'market', minimized: !!s.minimized
|
|
395
|
+
}
|
|
390
396
|
}
|
|
391
397
|
|
|
392
398
|
async function actUiSet(args) {
|
|
@@ -396,6 +402,14 @@ export default {
|
|
|
396
402
|
}
|
|
397
403
|
if (args.tab !== undefined) s.tab = String(args.tab)
|
|
398
404
|
if (args.minimized !== undefined) s.minimized = !!args.minimized
|
|
405
|
+
if (args.mode === 'floating' || args.mode === 'docked') s.mode = args.mode
|
|
406
|
+
if (args.size && typeof args.size === 'object' && isFinite(Number(args.size.w)) && isFinite(Number(args.size.h))) {
|
|
407
|
+
s.size = { w: Number(args.size.w), h: Number(args.size.h) }
|
|
408
|
+
}
|
|
409
|
+
if (args.dockW !== undefined && isFinite(Number(args.dockW))) s.dockW = Number(args.dockW)
|
|
410
|
+
if ('snap' in args && args.snap !== undefined) s.snap = args.snap === null ? null : String(args.snap)
|
|
411
|
+
if (args.snapEdge === 'left' || args.snapEdge === 'right' || args.snapEdge === null) s.snapEdge = args.snapEdge
|
|
412
|
+
if (args.snapW !== undefined && isFinite(Number(args.snapW))) s.snapW = Number(args.snapW)
|
|
399
413
|
await saveUiState()
|
|
400
414
|
return { ok: true }
|
|
401
415
|
}
|