dsh-xueqiu 1.1.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 +3 -1
- package/dynamic/client.js +105 -28
- package/dynamic/host.js +3 -0
- package/package.json +1 -1
- package/src/client/index.js +105 -28
- 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,7 +340,8 @@ 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
|
|
|
@@ -524,7 +526,7 @@ return {
|
|
|
524
526
|
]),
|
|
525
527
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
526
528
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
527
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
529
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
528
530
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
529
531
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
530
532
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -732,11 +734,12 @@ return {
|
|
|
732
734
|
return el('div', null, [tabs, errBox, content])
|
|
733
735
|
}
|
|
734
736
|
|
|
735
|
-
// ---------- 悬浮外壳(shell.overlay):自由拖拽 +
|
|
737
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
736
738
|
function FloatingShell() {
|
|
737
739
|
const [, force] = React.useState(0)
|
|
738
740
|
const [fIdx, setFIdx] = React.useState([])
|
|
739
741
|
const [fOpen, setFOpen] = React.useState(true)
|
|
742
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
740
743
|
React.useEffect(function () {
|
|
741
744
|
let alive = true
|
|
742
745
|
function refresh() {
|
|
@@ -760,6 +763,8 @@ return {
|
|
|
760
763
|
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
761
764
|
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
762
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) })
|
|
763
768
|
if (d && d.tab) ui.set({ tab: d.tab })
|
|
764
769
|
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
765
770
|
}).catch(function () { ui.set({ hydrated: true }) })
|
|
@@ -776,25 +781,47 @@ return {
|
|
|
776
781
|
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
777
782
|
const s = ui.size
|
|
778
783
|
|
|
779
|
-
//
|
|
784
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
780
785
|
function onTitleDown(e) {
|
|
781
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
|
+
}
|
|
782
796
|
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
783
797
|
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
784
798
|
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
785
799
|
ui.set({ snap: null })
|
|
800
|
+
setDragTarget(null)
|
|
786
801
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
787
802
|
}
|
|
788
803
|
function onTitleMove(e) {
|
|
789
804
|
if (!dragRef) return
|
|
790
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
|
+
}
|
|
791
812
|
}
|
|
792
813
|
function onTitleUp(e) {
|
|
793
814
|
dragRef = null
|
|
794
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)
|
|
795
822
|
snapCheck()
|
|
796
823
|
}
|
|
797
|
-
//
|
|
824
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
798
825
|
function snapCheck() {
|
|
799
826
|
const vp = viewport()
|
|
800
827
|
const p = ui.pos
|
|
@@ -813,24 +840,36 @@ return {
|
|
|
813
840
|
}
|
|
814
841
|
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
815
842
|
}
|
|
816
|
-
//
|
|
843
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
817
844
|
function toggleSnap() {
|
|
818
845
|
const vp = viewport()
|
|
819
|
-
if (ui.
|
|
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
|
+
}
|
|
820
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) } }) }
|
|
821
853
|
else { ui.set({ snap: 'br', pos: null }) }
|
|
822
854
|
}
|
|
823
|
-
//
|
|
855
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
824
856
|
function onRsDown(e) {
|
|
825
857
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
826
|
-
resizeRef =
|
|
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 }
|
|
827
861
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
828
862
|
}
|
|
829
863
|
function onRsMove(e) {
|
|
830
864
|
if (!resizeRef) return
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
+
}
|
|
834
873
|
}
|
|
835
874
|
function onRsUp(e) {
|
|
836
875
|
resizeRef = null
|
|
@@ -840,41 +879,67 @@ return {
|
|
|
840
879
|
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
841
880
|
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
842
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
|
+
|
|
843
891
|
// 最小化条
|
|
844
892
|
if (ui.minimized) {
|
|
845
893
|
const idxLine = fIdx.map(function (q) {
|
|
846
894
|
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
847
895
|
}).join(' · ')
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
el('
|
|
854
|
-
|
|
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
|
|
855
909
|
])
|
|
856
910
|
}
|
|
857
911
|
|
|
912
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
858
913
|
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
859
914
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
860
915
|
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
861
|
-
el('span', { key: 'upd', className: 'xq-update' }, '
|
|
916
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
862
917
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
863
|
-
el('button', { key: 'snap', className: 'xq-btn-mini', title:
|
|
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' ? '📌' : '📍'),
|
|
864
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 }) } }, '嵌入'),
|
|
865
920
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
866
921
|
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
867
922
|
])
|
|
868
923
|
|
|
869
|
-
return el('div', { className: 'xq-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
el(
|
|
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 })
|
|
873
931
|
]),
|
|
874
|
-
|
|
932
|
+
snapPreview
|
|
875
933
|
])
|
|
876
934
|
}
|
|
877
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
|
+
|
|
878
943
|
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
879
944
|
function DockedShell() {
|
|
880
945
|
const [, force] = React.useState(0)
|
|
@@ -989,7 +1054,19 @@ return {
|
|
|
989
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' +
|
|
990
1055
|
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
991
1056
|
'.xq-resize-w::after{display:none;}\n' +
|
|
992
|
-
'.xq-snapped{transition:left .16s ease, top .16s ease;}\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'
|
|
993
1070
|
)
|
|
994
1071
|
|
|
995
1072
|
// ---------- 注册 ----------
|
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,7 +345,8 @@ 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
|
|
|
@@ -529,7 +531,7 @@ export default {
|
|
|
529
531
|
]),
|
|
530
532
|
el('div', { key: 'c', className: colorOf(q.percent) }, fmt(q.current)),
|
|
531
533
|
el('div', { key: 'p', className: colorOf(q.percent) }, fmtPct(q.percent)),
|
|
532
|
-
el('div', { key: 'v', className: 'xq-sub' }, fmtVol(q.volume)),
|
|
534
|
+
el('div', { key: 'v', className: 'xq-sub xq-vol-col' }, fmtVol(q.volume)),
|
|
533
535
|
el('div', { key: 'a2', className: 'xq-actions' }, [
|
|
534
536
|
el('button', { key: 'd', className: 'xq-btn-mini', onClick: function () { openDetail(q.symbol) } }, '详情'),
|
|
535
537
|
el('button', { key: 'r', className: 'xq-btn-mini', onClick: function () { removeWatch(q.symbol) } }, '×')
|
|
@@ -737,11 +739,12 @@ export default {
|
|
|
737
739
|
return el('div', null, [tabs, errBox, content])
|
|
738
740
|
}
|
|
739
741
|
|
|
740
|
-
// ---------- 悬浮外壳(shell.overlay):自由拖拽 +
|
|
742
|
+
// ---------- 悬浮外壳(shell.overlay):自由拖拽 + 左右边缘贴靠 + 四角磁吸 + 缩放 ----------
|
|
741
743
|
function FloatingShell() {
|
|
742
744
|
const [, force] = React.useState(0)
|
|
743
745
|
const [fIdx, setFIdx] = React.useState([])
|
|
744
746
|
const [fOpen, setFOpen] = React.useState(true)
|
|
747
|
+
const [dragTarget, setDragTarget] = React.useState(null)
|
|
745
748
|
React.useEffect(function () {
|
|
746
749
|
let alive = true
|
|
747
750
|
function refresh() {
|
|
@@ -765,6 +768,8 @@ export default {
|
|
|
765
768
|
if (d && d.size) ui.set({ size: { w: Number(d.size.w), h: Number(d.size.h) } })
|
|
766
769
|
if (d && d.dockW) ui.set({ dockW: Number(d.dockW) })
|
|
767
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) })
|
|
768
773
|
if (d && d.tab) ui.set({ tab: d.tab })
|
|
769
774
|
ui.set({ minimized: !!(d && d.minimized), hydrated: true })
|
|
770
775
|
}).catch(function () { ui.set({ hydrated: true }) })
|
|
@@ -781,25 +786,47 @@ export default {
|
|
|
781
786
|
if (ui.mode !== 'floating' || ui.closed || !ui.hydrated) return null
|
|
782
787
|
const s = ui.size
|
|
783
788
|
|
|
784
|
-
//
|
|
789
|
+
// 拖拽(贴靠状态下按下标题栏 → 解除贴靠回到自由)
|
|
785
790
|
function onTitleDown(e) {
|
|
786
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
|
+
}
|
|
787
801
|
const baseX = ui.pos ? ui.pos.x : e.clientX
|
|
788
802
|
const baseY = ui.pos ? ui.pos.y : e.clientY
|
|
789
803
|
dragRef = { dx: e.clientX - baseX, dy: e.clientY - baseY }
|
|
790
804
|
ui.set({ snap: null })
|
|
805
|
+
setDragTarget(null)
|
|
791
806
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
792
807
|
}
|
|
793
808
|
function onTitleMove(e) {
|
|
794
809
|
if (!dragRef) return
|
|
795
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
|
+
}
|
|
796
817
|
}
|
|
797
818
|
function onTitleUp(e) {
|
|
798
819
|
dragRef = null
|
|
799
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)
|
|
800
827
|
snapCheck()
|
|
801
828
|
}
|
|
802
|
-
//
|
|
829
|
+
// 四角磁吸:拖放终点靠近任一屏幕角 → 吸附
|
|
803
830
|
function snapCheck() {
|
|
804
831
|
const vp = viewport()
|
|
805
832
|
const p = ui.pos
|
|
@@ -818,24 +845,36 @@ export default {
|
|
|
818
845
|
}
|
|
819
846
|
ui.set({ pos: best ? { x: Math.max(0, best.x), y: Math.max(0, best.y) } : p, snap: best ? best.key : null })
|
|
820
847
|
}
|
|
821
|
-
//
|
|
848
|
+
// 📌 循环:右下角 → 右侧窄栏贴靠 → 解除
|
|
822
849
|
function toggleSnap() {
|
|
823
850
|
const vp = viewport()
|
|
824
|
-
if (ui.
|
|
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
|
+
}
|
|
825
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) } }) }
|
|
826
858
|
else { ui.set({ snap: 'br', pos: null }) }
|
|
827
859
|
}
|
|
828
|
-
//
|
|
860
|
+
// 缩放(右下角手柄;贴靠时只调宽度)
|
|
829
861
|
function onRsDown(e) {
|
|
830
862
|
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
831
|
-
resizeRef =
|
|
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 }
|
|
832
866
|
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* ignore */ }
|
|
833
867
|
}
|
|
834
868
|
function onRsMove(e) {
|
|
835
869
|
if (!resizeRef) return
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
}
|
|
839
878
|
}
|
|
840
879
|
function onRsUp(e) {
|
|
841
880
|
resizeRef = null
|
|
@@ -845,41 +884,67 @@ export default {
|
|
|
845
884
|
const dragProps = { onPointerDown: onTitleDown, onPointerMove: onTitleMove, onPointerUp: onTitleUp, onPointerCancel: onTitleUp }
|
|
846
885
|
const rsProps = { onPointerDown: onRsDown, onPointerMove: onRsMove, onPointerUp: onRsUp, onPointerCancel: onRsUp }
|
|
847
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
|
+
|
|
848
896
|
// 最小化条
|
|
849
897
|
if (ui.minimized) {
|
|
850
898
|
const idxLine = fIdx.map(function (q) {
|
|
851
899
|
return q.name + ' ' + fmt(q.current) + ' ' + fmtPct(q.percent)
|
|
852
900
|
}).join(' · ')
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
el('
|
|
859
|
-
|
|
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
|
|
860
914
|
])
|
|
861
915
|
}
|
|
862
916
|
|
|
917
|
+
const snapped = ui.snapEdge ? ' xq-snapped xq-narrow' : (ui.snap ? ' xq-snapped' : '')
|
|
863
918
|
const titleBar = el('div', { className: 'xq-title', ...dragProps }, [
|
|
864
919
|
el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
|
|
865
920
|
el('span', { key: 'st', className: 'xq-status' + (fOpen ? ' xq-live' : '') }, fOpen ? '● 盘中' : '已收盘'),
|
|
866
|
-
el('span', { key: 'upd', className: 'xq-update' }, '
|
|
921
|
+
el('span', { key: 'upd', className: 'xq-update' }, ui.snapEdge === 'right' ? '已贴右 · 拖走解除' : ui.snapEdge === 'left' ? '已贴左 · 拖走解除' : '拖到左/右边缘贴靠'),
|
|
867
922
|
el('span', { key: 'sp', className: 'xq-spacer' }),
|
|
868
|
-
el('button', { key: 'snap', className: 'xq-btn-mini', title:
|
|
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' ? '📌' : '📍'),
|
|
869
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 }) } }, '嵌入'),
|
|
870
925
|
el('button', { key: 'min', className: 'xq-btn-mini', title: '收起', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ minimized: true }) } }, '—'),
|
|
871
926
|
el('button', { key: 'cls', className: 'xq-btn-mini', title: '关闭(点底部指数条重新打开)', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕')
|
|
872
927
|
])
|
|
873
928
|
|
|
874
|
-
return el('div', { className: 'xq-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
el(
|
|
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 })
|
|
878
936
|
]),
|
|
879
|
-
|
|
937
|
+
snapPreview
|
|
880
938
|
])
|
|
881
939
|
}
|
|
882
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
|
+
|
|
883
948
|
// ---------- 嵌入外壳(conversation.input.dock):输入框上方,不遮挡对话 ----------
|
|
884
949
|
function DockedShell() {
|
|
885
950
|
const [, force] = React.useState(0)
|
|
@@ -994,7 +1059,19 @@ export default {
|
|
|
994
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' +
|
|
995
1060
|
'.xq-resize-w{position:absolute;right:0;top:0;bottom:0;width:10px;cursor:ew-resize;z-index:6;}\n' +
|
|
996
1061
|
'.xq-resize-w::after{display:none;}\n' +
|
|
997
|
-
'.xq-snapped{transition:left .16s ease, top .16s ease;}\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'
|
|
998
1075
|
)
|
|
999
1076
|
|
|
1000
1077
|
// ---------- 注册 ----------
|
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
|
}
|