dsh-xueqiu 1.1.0 → 1.2.1
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 +3 -1
- package/dynamic/client.js +108 -29
- package/dynamic/host.js +3 -0
- package/package.json +1 -1
- package/src/client/index.js +108 -29
- package/src/index.js +3 -0
package/README.md
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
| 📰 快讯 | 7×24 实时快讯,重要新闻高亮 |
|
|
19
19
|
| 👥 热议用户 | 个股热门 KOL(粉丝数/认证标识) |
|
|
20
20
|
| 💼 自选股 | 本地持久化,增删随点随改 |
|
|
21
|
-
| 🖱️ 悬浮面板 |
|
|
21
|
+
| 🖱️ 悬浮面板 | 按住标题栏自由拖拽,**拖到左/右屏幕边缘自动贴靠为窄栏**(Windows 贴靠风格,380px 可调),**拖到角落自动磁吸**,📌 一键贴右下角/右侧窄栏,**右下角手柄自由缩放宽高** |
|
|
22
|
+
| 🪟 贴靠助手 | 拖动时在屏幕边缘显示**半透明贴靠预览**,松手即吸附;贴靠后拖动标题栏解除 |
|
|
23
|
+
| 📐 窄栏自适应 | 贴靠窄栏自动切换紧凑布局(隐藏次要列、指数卡缩排、K线/表格自适应) |
|
|
22
24
|
| 📌 嵌入模式 | 一键切换到**输入框上方**(页面布局内,**不遮挡对话记录**),宽度可调,随页面滚动 |
|
|
23
25
|
| ⏱️ 智能刷新 | 盘中 20s 刷新行情,收盘自动放慢,降低被风控概率 |
|
|
24
26
|
| 🌗 主题自适应 | 跟随 DSH 明暗主题 |
|
package/dynamic/client.js
CHANGED
|
@@ -306,7 +306,8 @@ return {
|
|
|
306
306
|
// ---------- 共享 UI 状态(悬浮/嵌入/位置/尺寸/标签页) ----------
|
|
307
307
|
const ui = {
|
|
308
308
|
mode: 'floating', pos: null, size: { w: 720, h: 640 }, dockW: 720,
|
|
309
|
-
snap: null,
|
|
309
|
+
snap: null, snapEdge: null, snapW: 380,
|
|
310
|
+
minimized: false, tab: 'market', closed: false, hydrated: false,
|
|
310
311
|
_fns: [],
|
|
311
312
|
subscribe: function (fn) {
|
|
312
313
|
this._fns.push(fn)
|
|
@@ -339,13 +340,16 @@ return {
|
|
|
339
340
|
const saveUi = ctx.debounce(function () {
|
|
340
341
|
call('ui.set', {
|
|
341
342
|
mode: ui.mode, pos: ui.pos, size: ui.size, dockW: ui.dockW,
|
|
342
|
-
snap: ui.snap,
|
|
343
|
+
snap: ui.snap, snapEdge: ui.snapEdge, snapW: ui.snapW,
|
|
344
|
+
tab: ui.tab, minimized: ui.minimized
|
|
343
345
|
}).catch(function () { /* 忽略 */ })
|
|
344
346
|
}, 800)
|
|
345
347
|
|
|
346
348
|
// 拖拽 / 缩放用可变引用(实例唯一)
|
|
347
349
|
let dragRef = null
|
|
348
350
|
let resizeRef = null
|
|
351
|
+
let dragStart = null
|
|
352
|
+
let suppressSnap = false
|
|
349
353
|
|
|
350
354
|
// ---------- 面板内容(数据 + tabs + 详情),由悬浮/嵌入外壳共用 ----------
|
|
351
355
|
function XueqiuPanel() {
|
|
@@ -524,7 +528,7 @@ return {
|
|
|
524
528
|
]),
|
|
525
529
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
526
530
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
527
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
531
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
528
532
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
529
533
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
530
534
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -732,11 +736,12 @@ return {
|
|
|
732
736
|
return el('div', null, [tabs, errBox, content])
|
|
733
737
|
}
|
|
734
738
|
|
|
735
|
-
// ---------- 悬浮外壳(shell.overlay):自由拖拽 +
|
|
739
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
736
740
|
function FloatingShell() {
|
|
737
741
|
const [, force] = React.useState(0)
|
|
738
742
|
const [fIdx, setFIdx] = React.useState([])
|
|
739
743
|
const [fOpen, setFOpen] = React.useState(true)
|
|
744
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
740
745
|
React.useEffect(function () {
|
|
741
746
|
let alive = true
|
|
742
747
|
function refresh() {
|
|
@@ -760,6 +765,8 @@ return {
|
|
|
760
765
|
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
761
766
|
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
762
767
|
if (d && d.snap) ui.set({ snap: d.snap })
|
|
768
|
+
if (d && (d.snapEdge === 'left' || d.snapEdge === 'right')) ui.set({ snapEdge: d.snapEdge })
|
|
769
|
+
if (d && d.snapW) ui.set({ snapW: Number(d.snapW) })
|
|
763
770
|
if (d && d.tab) ui.set({ tab: d.tab })
|
|
764
771
|
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
765
772
|
}).catch(function () { ui.set({ hydrated: true }) })
|
|
@@ -776,25 +783,47 @@ return {
|
|
|
776
783
|
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
777
784
|
const s = ui.size
|
|
778
785
|
|
|
779
|
-
//
|
|
786
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
780
787
|
function onTitleDown(e) {
|
|
781
788
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
789
|
+
dragStart = { x: e.clientX, y: e.clientY }
|
|
790
|
+
if (ui.snapEdge) {
|
|
791
|
+
// 解除贴靠:面板从按下位置转为自由,本次松手不再磁吸
|
|
792
|
+
ui.set({ snapEdge: null, snap: null, pos: { x: e.clientX - 30, y: e.clientY - 16 } })
|
|
793
|
+
suppressSnap = true
|
|
794
|
+
}
|
|
782
795
|
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
783
796
|
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
784
797
|
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
785
798
|
ui.set({ snap: null })
|
|
799
|
+
setDragTarget(null)
|
|
786
800
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
787
801
|
}
|
|
788
802
|
function onTitleMove(e) {
|
|
789
803
|
if (!dragRef) return
|
|
790
804
|
ui.set({ pos: { x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) } })
|
|
805
|
+
const vp = viewport()
|
|
806
|
+
if (vp) {
|
|
807
|
+
if (e.clientX >= vp.w - 70) setDragTarget('right')
|
|
808
|
+
else if (e.clientX <= 70) setDragTarget('left')
|
|
809
|
+
else setDragTarget(null)
|
|
810
|
+
}
|
|
791
811
|
}
|
|
792
812
|
function onTitleUp(e) {
|
|
813
|
+
const moved = dragStart ? Math.sqrt(Math.pow(e.clientX - dragStart.x, 2) + Math.pow(e.clientY - dragStart.y, 2)) : 0
|
|
793
814
|
dragRef = null
|
|
815
|
+
dragStart = null
|
|
794
816
|
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
795
|
-
|
|
817
|
+
if (suppressSnap) { suppressSnap = false; setDragTarget(null); return }
|
|
818
|
+
if (dragTarget) {
|
|
819
|
+
ui.set({ snapEdge: dragTarget, pos: null, snap: null, snapW: ui.snapW || 380 })
|
|
820
|
+
setDragTarget(null)
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
setDragTarget(null)
|
|
824
|
+
if (moved >= 6) snapCheck()
|
|
796
825
|
}
|
|
797
|
-
//
|
|
826
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
798
827
|
function snapCheck() {
|
|
799
828
|
const vp = viewport()
|
|
800
829
|
const p = ui.pos
|
|
@@ -813,24 +842,36 @@ return {
|
|
|
813
842
|
}
|
|
814
843
|
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
815
844
|
}
|
|
816
|
-
//
|
|
845
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
817
846
|
function toggleSnap() {
|
|
818
847
|
const vp = viewport()
|
|
819
|
-
if (ui.
|
|
848
|
+
if (ui.snapEdge === 'right') { ui.set({ snapEdge: null }); return }
|
|
849
|
+
if (ui.snap === 'br') {
|
|
850
|
+
ui.set({ snap: null })
|
|
851
|
+
if (vp) ui.set({ snapEdge: 'right', pos: null, snapW: ui.snapW || 380 })
|
|
852
|
+
return
|
|
853
|
+
}
|
|
820
854
|
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) } }) }
|
|
821
855
|
else { ui.set({ snap: 'br', pos: null }) }
|
|
822
856
|
}
|
|
823
|
-
//
|
|
857
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
824
858
|
function onRsDown(e) {
|
|
825
859
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
826
|
-
resizeRef =
|
|
860
|
+
resizeRef = ui.snapEdge
|
|
861
|
+
? { w: ui.snapW, h: 0, x: e.clientX, y: e.clientY }
|
|
862
|
+
: { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
|
|
827
863
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
828
864
|
}
|
|
829
865
|
function onRsMove(e) {
|
|
830
866
|
if (!resizeRef) return
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
867
|
+
if (ui.snapEdge) {
|
|
868
|
+
const w = Math.min(Math.max(300, resizeRef.w + (e.clientX - resizeRef.x)), 560)
|
|
869
|
+
ui.set({ snapW: w })
|
|
870
|
+
} else {
|
|
871
|
+
const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
|
|
872
|
+
const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
|
|
873
|
+
ui.set({ size: { w: w, h: h }, snap: null })
|
|
874
|
+
}
|
|
834
875
|
}
|
|
835
876
|
function onRsUp(e) {
|
|
836
877
|
resizeRef = null
|
|
@@ -840,41 +881,67 @@ return {
|
|
|
840
881
|
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
841
882
|
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
842
883
|
|
|
884
|
+
// 贴靠助手预览(拖动中靠近边缘时显示)
|
|
885
|
+
let snapPreview = null
|
|
886
|
+
if (dragTarget) {
|
|
887
|
+
snapPreview = el('div', {
|
|
888
|
+
key: 'prev', className: 'xq-snap-preview xq-snap-' + dragTarget,
|
|
889
|
+
style: { width: ui.snapW || 380 }
|
|
890
|
+
})
|
|
891
|
+
}
|
|
892
|
+
|
|
843
893
|
// 最小化条
|
|
844
894
|
if (ui.minimized) {
|
|
845
895
|
const idxLine = fIdx.map(function (q) {
|
|
846
896
|
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
847
897
|
}).join(' · ')
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
el('
|
|
854
|
-
|
|
898
|
+
let minStyle = null
|
|
899
|
+
if (ui.snapEdge === 'right') minStyle = { right: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
900
|
+
else if (ui.snapEdge === 'left') minStyle = { left: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
901
|
+
else if (ui.pos) minStyle = { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' }
|
|
902
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
903
|
+
el('div', { key: 'min', className: 'xq-float xq-min' + (ui.snapEdge ? ' xq-snapped' : ''), style: minStyle, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
904
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
905
|
+
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
906
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
907
|
+
el('button', { key: 'x', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
|
|
908
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
909
|
+
]),
|
|
910
|
+
snapPreview
|
|
855
911
|
])
|
|
856
912
|
}
|
|
857
913
|
|
|
914
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
858
915
|
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
859
916
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
860
917
|
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
861
|
-
el('span', { key: 'upd', className: 'xq-update' }, '
|
|
918
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
862
919
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
863
|
-
el('button', { key: 'snap', className: 'xq-btn-mini', title:
|
|
920
|
+
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' ? '📌' : '📍'),
|
|
864
921
|
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 }) } }, '嵌入'),
|
|
865
922
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
866
923
|
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
867
924
|
])
|
|
868
925
|
|
|
869
|
-
return el('div', { className: 'xq-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
el(
|
|
926
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
927
|
+
el('div', { key: 'f', className: 'xq-float' + snapped, style: posStyle() }, [
|
|
928
|
+
titleBar,
|
|
929
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
930
|
+
el(XueqiuPanel, null)
|
|
931
|
+
]),
|
|
932
|
+
el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
|
|
873
933
|
]),
|
|
874
|
-
|
|
934
|
+
snapPreview
|
|
875
935
|
])
|
|
876
936
|
}
|
|
877
937
|
|
|
938
|
+
function posStyle() {
|
|
939
|
+
if (ui.snapEdge === 'right') return { right: 0, top: 0, bottom: 0, left: 'auto', width: ui.snapW }
|
|
940
|
+
if (ui.snapEdge === 'left') return { left: 0, top: 0, bottom: 0, right: 'auto', width: ui.snapW }
|
|
941
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
942
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
943
|
+
}
|
|
944
|
+
|
|
878
945
|
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
879
946
|
function DockedShell() {
|
|
880
947
|
const [, force] = React.useState(0)
|
|
@@ -989,7 +1056,19 @@ return {
|
|
|
989
1056
|
'.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' +
|
|
990
1057
|
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
991
1058
|
'.xq-resize-w::after{display:none;}\n' +
|
|
992
|
-
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n'
|
|
1059
|
+
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n' +
|
|
1060
|
+
'.xq-wrap{pointer-events:none;position:fixed;inset:0;z-index:1200;}\n' +
|
|
1061
|
+
'.xq-wrap .xq-float{pointer-events:auto;}\n' +
|
|
1062
|
+
'.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' +
|
|
1063
|
+
'.xq-snap-right{right:8px;}\n' +
|
|
1064
|
+
'.xq-snap-left{left:8px;}\n' +
|
|
1065
|
+
'.xq-float.xq-narrow .xq-grid-hd,.xq-float.xq-narrow .xq-grid-row{grid-template-columns:1.5fr 1fr 1fr 62px;}\n' +
|
|
1066
|
+
'.xq-float.xq-narrow .xq-vol-col{display:none;}\n' +
|
|
1067
|
+
'.xq-float.xq-narrow .xq-idx-card{min-width:88px;padding:5px 6px;}\n' +
|
|
1068
|
+
'.xq-float.xq-narrow .xq-idx-cur{font-size:12px;}\n' +
|
|
1069
|
+
'.xq-float.xq-narrow .xq-stats{grid-template-columns:repeat(2,1fr);}\n' +
|
|
1070
|
+
'.xq-float.xq-narrow .xq-detail-cur{font-size:16px;}\n' +
|
|
1071
|
+
'.xq-float.xq-narrow .xq-periods button,.xq-float.xq-narrow .xq-modes button{padding:2px 6px;}\n'
|
|
993
1072
|
)
|
|
994
1073
|
|
|
995
1074
|
// ---------- 注册 ----------
|
package/dynamic/host.js
CHANGED
|
@@ -389,6 +389,7 @@ return {
|
|
|
389
389
|
mode: s.mode || 'floating', pos: s.pos || null,
|
|
390
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
391
|
dockW: s.dockW || null, snap: s.snap || null,
|
|
392
|
+
snapEdge: s.snapEdge || null, snapW: s.snapW || 380,
|
|
392
393
|
tab: s.tab || 'market', minimized: !!s.minimized
|
|
393
394
|
}
|
|
394
395
|
}
|
|
@@ -406,6 +407,8 @@ return {
|
|
|
406
407
|
}
|
|
407
408
|
if (args.dockW !== undefined && isFinite(Number(args.dockW))) s.dockW = Number(args.dockW)
|
|
408
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)
|
|
409
412
|
await saveUiState()
|
|
410
413
|
return { ok: true }
|
|
411
414
|
}
|
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -311,7 +311,8 @@ export default {
|
|
|
311
311
|
// ---------- 共享 UI 状态(悬浮/嵌入/位置/尺寸/标签页) ----------
|
|
312
312
|
const ui = {
|
|
313
313
|
mode: 'floating', pos: null, size: { w: 720, h: 640 }, dockW: 720,
|
|
314
|
-
snap: null,
|
|
314
|
+
snap: null, snapEdge: null, snapW: 380,
|
|
315
|
+
minimized: false, tab: 'market', closed: false, hydrated: false,
|
|
315
316
|
_fns: [],
|
|
316
317
|
subscribe: function (fn) {
|
|
317
318
|
this._fns.push(fn)
|
|
@@ -344,13 +345,16 @@ export default {
|
|
|
344
345
|
const saveUi = ctx.debounce(function () {
|
|
345
346
|
call('ui.set', {
|
|
346
347
|
mode: ui.mode, pos: ui.pos, size: ui.size, dockW: ui.dockW,
|
|
347
|
-
snap: ui.snap,
|
|
348
|
+
snap: ui.snap, snapEdge: ui.snapEdge, snapW: ui.snapW,
|
|
349
|
+
tab: ui.tab, minimized: ui.minimized
|
|
348
350
|
}).catch(function () { /* 忽略 */ })
|
|
349
351
|
}, 800)
|
|
350
352
|
|
|
351
353
|
// 拖拽 / 缩放用可变引用(实例唯一)
|
|
352
354
|
let dragRef = null
|
|
353
355
|
let resizeRef = null
|
|
356
|
+
let dragStart = null
|
|
357
|
+
let suppressSnap = false
|
|
354
358
|
|
|
355
359
|
// ---------- 面板内容(数据 + tabs + 详情),由悬浮/嵌入外壳共用 ----------
|
|
356
360
|
function XueqiuPanel() {
|
|
@@ -529,7 +533,7 @@ export default {
|
|
|
529
533
|
]),
|
|
530
534
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
531
535
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
532
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
536
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
533
537
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
534
538
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
535
539
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -737,11 +741,12 @@ export default {
|
|
|
737
741
|
return el('div', null, [tabs, errBox, content])
|
|
738
742
|
}
|
|
739
743
|
|
|
740
|
-
// ---------- 悬浮外壳(shell.overlay):自由拖拽 +
|
|
744
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
741
745
|
function FloatingShell() {
|
|
742
746
|
const [, force] = React.useState(0)
|
|
743
747
|
const [fIdx, setFIdx] = React.useState([])
|
|
744
748
|
const [fOpen, setFOpen] = React.useState(true)
|
|
749
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
745
750
|
React.useEffect(function () {
|
|
746
751
|
let alive = true
|
|
747
752
|
function refresh() {
|
|
@@ -765,6 +770,8 @@ export default {
|
|
|
765
770
|
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
766
771
|
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
767
772
|
if (d && d.snap) ui.set({ snap: d.snap })
|
|
773
|
+
if (d && (d.snapEdge === 'left' || d.snapEdge === 'right')) ui.set({ snapEdge: d.snapEdge })
|
|
774
|
+
if (d && d.snapW) ui.set({ snapW: Number(d.snapW) })
|
|
768
775
|
if (d && d.tab) ui.set({ tab: d.tab })
|
|
769
776
|
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
770
777
|
}).catch(function () { ui.set({ hydrated: true }) })
|
|
@@ -781,25 +788,47 @@ export default {
|
|
|
781
788
|
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
782
789
|
const s = ui.size
|
|
783
790
|
|
|
784
|
-
//
|
|
791
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
785
792
|
function onTitleDown(e) {
|
|
786
793
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
794
|
+
dragStart = { x: e.clientX, y: e.clientY }
|
|
795
|
+
if (ui.snapEdge) {
|
|
796
|
+
// 解除贴靠:面板从按下位置转为自由,本次松手不再磁吸
|
|
797
|
+
ui.set({ snapEdge: null, snap: null, pos: { x: e.clientX - 30, y: e.clientY - 16 } })
|
|
798
|
+
suppressSnap = true
|
|
799
|
+
}
|
|
787
800
|
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
788
801
|
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
789
802
|
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
790
803
|
ui.set({ snap: null })
|
|
804
|
+
setDragTarget(null)
|
|
791
805
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
792
806
|
}
|
|
793
807
|
function onTitleMove(e) {
|
|
794
808
|
if (!dragRef) return
|
|
795
809
|
ui.set({ pos: { x: Math.max(4, e.clientX - dragRef.dx), y: Math.max(4, e.clientY - dragRef.dy) } })
|
|
810
|
+
const vp = viewport()
|
|
811
|
+
if (vp) {
|
|
812
|
+
if (e.clientX >= vp.w - 70) setDragTarget('right')
|
|
813
|
+
else if (e.clientX <= 70) setDragTarget('left')
|
|
814
|
+
else setDragTarget(null)
|
|
815
|
+
}
|
|
796
816
|
}
|
|
797
817
|
function onTitleUp(e) {
|
|
818
|
+
const moved = dragStart ? Math.sqrt(Math.pow(e.clientX - dragStart.x, 2) + Math.pow(e.clientY - dragStart.y, 2)) : 0
|
|
798
819
|
dragRef = null
|
|
820
|
+
dragStart = null
|
|
799
821
|
try { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
800
|
-
|
|
822
|
+
if (suppressSnap) { suppressSnap = false; setDragTarget(null); return }
|
|
823
|
+
if (dragTarget) {
|
|
824
|
+
ui.set({ snapEdge: dragTarget, pos: null, snap: null, snapW: ui.snapW || 380 })
|
|
825
|
+
setDragTarget(null)
|
|
826
|
+
return
|
|
827
|
+
}
|
|
828
|
+
setDragTarget(null)
|
|
829
|
+
if (moved >= 6) snapCheck()
|
|
801
830
|
}
|
|
802
|
-
//
|
|
831
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
803
832
|
function snapCheck() {
|
|
804
833
|
const vp = viewport()
|
|
805
834
|
const p = ui.pos
|
|
@@ -818,24 +847,36 @@ export default {
|
|
|
818
847
|
}
|
|
819
848
|
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
820
849
|
}
|
|
821
|
-
//
|
|
850
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
822
851
|
function toggleSnap() {
|
|
823
852
|
const vp = viewport()
|
|
824
|
-
if (ui.
|
|
853
|
+
if (ui.snapEdge === 'right') { ui.set({ snapEdge: null }); return }
|
|
854
|
+
if (ui.snap === 'br') {
|
|
855
|
+
ui.set({ snap: null })
|
|
856
|
+
if (vp) ui.set({ snapEdge: 'right', pos: null, snapW: ui.snapW || 380 })
|
|
857
|
+
return
|
|
858
|
+
}
|
|
825
859
|
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) } }) }
|
|
826
860
|
else { ui.set({ snap: 'br', pos: null }) }
|
|
827
861
|
}
|
|
828
|
-
//
|
|
862
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
829
863
|
function onRsDown(e) {
|
|
830
864
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
831
|
-
resizeRef =
|
|
865
|
+
resizeRef = ui.snapEdge
|
|
866
|
+
? { w: ui.snapW, h: 0, x: e.clientX, y: e.clientY }
|
|
867
|
+
: { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
|
|
832
868
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
833
869
|
}
|
|
834
870
|
function onRsMove(e) {
|
|
835
871
|
if (!resizeRef) return
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
872
|
+
if (ui.snapEdge) {
|
|
873
|
+
const w = Math.min(Math.max(300, resizeRef.w + (e.clientX - resizeRef.x)), 560)
|
|
874
|
+
ui.set({ snapW: w })
|
|
875
|
+
} else {
|
|
876
|
+
const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
|
|
877
|
+
const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
|
|
878
|
+
ui.set({ size: { w: w, h: h }, snap: null })
|
|
879
|
+
}
|
|
839
880
|
}
|
|
840
881
|
function onRsUp(e) {
|
|
841
882
|
resizeRef = null
|
|
@@ -845,41 +886,67 @@ export default {
|
|
|
845
886
|
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
846
887
|
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
847
888
|
|
|
889
|
+
// 贴靠助手预览(拖动中靠近边缘时显示)
|
|
890
|
+
let snapPreview = null
|
|
891
|
+
if (dragTarget) {
|
|
892
|
+
snapPreview = el('div', {
|
|
893
|
+
key: 'prev', className: 'xq-snap-preview xq-snap-' + dragTarget,
|
|
894
|
+
style: { width: ui.snapW || 380 }
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
848
898
|
// 最小化条
|
|
849
899
|
if (ui.minimized) {
|
|
850
900
|
const idxLine = fIdx.map(function (q) {
|
|
851
901
|
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
852
902
|
}).join(' · ')
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
el('
|
|
859
|
-
|
|
903
|
+
let minStyle = null
|
|
904
|
+
if (ui.snapEdge === 'right') minStyle = { right: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
905
|
+
else if (ui.snapEdge === 'left') minStyle = { left: 0, top: 0, bottom: 0, width: ui.snapW, height: 'auto' }
|
|
906
|
+
else if (ui.pos) minStyle = { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' }
|
|
907
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
908
|
+
el('div', { key: 'min', className: 'xq-float xq-min' + (ui.snapEdge ? ' xq-snapped' : ''), style: minStyle, onClick: function () { ui.set({ minimized: false }) } }, [
|
|
909
|
+
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
910
|
+
el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
|
|
911
|
+
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
912
|
+
el('button', { key: 'x', className: 'xq-btn-mini', onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
|
|
913
|
+
el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
|
|
914
|
+
]),
|
|
915
|
+
snapPreview
|
|
860
916
|
])
|
|
861
917
|
}
|
|
862
918
|
|
|
919
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
863
920
|
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
864
921
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
865
922
|
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
866
|
-
el('span', { key: 'upd', className: 'xq-update' }, '
|
|
923
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
867
924
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
868
|
-
el('button', { key: 'snap', className: 'xq-btn-mini', title:
|
|
925
|
+
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' ? '📌' : '📍'),
|
|
869
926
|
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 }) } }, '嵌入'),
|
|
870
927
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
871
928
|
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
872
929
|
])
|
|
873
930
|
|
|
874
|
-
return el('div', { className: 'xq-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
el(
|
|
931
|
+
return el('div', { className: 'xq-wrap' }, [
|
|
932
|
+
el('div', { key: 'f', className: 'xq-float' + snapped, style: posStyle() }, [
|
|
933
|
+
titleBar,
|
|
934
|
+
el('div', { key: 'body', className: 'xq-body' }, [
|
|
935
|
+
el(XueqiuPanel, null)
|
|
936
|
+
]),
|
|
937
|
+
el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
|
|
878
938
|
]),
|
|
879
|
-
|
|
939
|
+
snapPreview
|
|
880
940
|
])
|
|
881
941
|
}
|
|
882
942
|
|
|
943
|
+
function posStyle() {
|
|
944
|
+
if (ui.snapEdge === 'right') return { right: 0, top: 0, bottom: 0, left: 'auto', width: ui.snapW }
|
|
945
|
+
if (ui.snapEdge === 'left') return { left: 0, top: 0, bottom: 0, right: 'auto', width: ui.snapW }
|
|
946
|
+
if (ui.pos) return { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto', width: ui.size.w, height: ui.size.h }
|
|
947
|
+
return { width: ui.size.w, height: ui.size.h }
|
|
948
|
+
}
|
|
949
|
+
|
|
883
950
|
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
884
951
|
function DockedShell() {
|
|
885
952
|
const [, force] = React.useState(0)
|
|
@@ -994,7 +1061,19 @@ export default {
|
|
|
994
1061
|
'.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' +
|
|
995
1062
|
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
996
1063
|
'.xq-resize-w::after{display:none;}\n' +
|
|
997
|
-
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n'
|
|
1064
|
+
'.xq-snapped{transition:left .16s ease, top .16s ease;}\n' +
|
|
1065
|
+
'.xq-wrap{pointer-events:none;position:fixed;inset:0;z-index:1200;}\n' +
|
|
1066
|
+
'.xq-wrap .xq-float{pointer-events:auto;}\n' +
|
|
1067
|
+
'.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' +
|
|
1068
|
+
'.xq-snap-right{right:8px;}\n' +
|
|
1069
|
+
'.xq-snap-left{left:8px;}\n' +
|
|
1070
|
+
'.xq-float.xq-narrow .xq-grid-hd,.xq-float.xq-narrow .xq-grid-row{grid-template-columns:1.5fr 1fr 1fr 62px;}\n' +
|
|
1071
|
+
'.xq-float.xq-narrow .xq-vol-col{display:none;}\n' +
|
|
1072
|
+
'.xq-float.xq-narrow .xq-idx-card{min-width:88px;padding:5px 6px;}\n' +
|
|
1073
|
+
'.xq-float.xq-narrow .xq-idx-cur{font-size:12px;}\n' +
|
|
1074
|
+
'.xq-float.xq-narrow .xq-stats{grid-template-columns:repeat(2,1fr);}\n' +
|
|
1075
|
+
'.xq-float.xq-narrow .xq-detail-cur{font-size:16px;}\n' +
|
|
1076
|
+
'.xq-float.xq-narrow .xq-periods button,.xq-float.xq-narrow .xq-modes button{padding:2px 6px;}\n'
|
|
998
1077
|
)
|
|
999
1078
|
|
|
1000
1079
|
// ---------- 注册 ----------
|
package/src/index.js
CHANGED
|
@@ -390,6 +390,7 @@ export default {
|
|
|
390
390
|
mode: s.mode || 'floating', pos: s.pos || null,
|
|
391
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
392
|
dockW: s.dockW || null, snap: s.snap || null,
|
|
393
|
+
snapEdge: s.snapEdge || null, snapW: s.snapW || 380,
|
|
393
394
|
tab: s.tab || 'market', minimized: !!s.minimized
|
|
394
395
|
}
|
|
395
396
|
}
|
|
@@ -407,6 +408,8 @@ export default {
|
|
|
407
408
|
}
|
|
408
409
|
if (args.dockW !== undefined && isFinite(Number(args.dockW))) s.dockW = Number(args.dockW)
|
|
409
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)
|
|
410
413
|
await saveUiState()
|
|
411
414
|
return { ok: true }
|
|
412
415
|
}
|