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 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, minimized: false, tab: 'market', closed: false, hydrated: false,
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, tab: ui.tab, minimized: ui.minimized
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.snap === 'br') { ui.set({ snap: null }); return }
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 = { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
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
- const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
832
- const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
833
- ui.set({ size: { w: w, h: h }, snap: null })
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
- const minStyle = ui.pos ? { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' } : null
849
- return el('div', { className: 'xq-float xq-min', style: minStyle, ...dragProps, onClick: function () { ui.set({ minimized: false }) } }, [
850
- el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
851
- el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
852
- el('span', { key: 'sp', className: 'xq-spacer' }),
853
- el('button', { key: 'x', className: 'xq-btn-mini', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
854
- el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
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: ui.snap === 'br' ? '解除右下角吸附' : '吸附到右下角', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snap === 'br' ? '📌' : '📍'),
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-float' + (ui.snap ? ' xq-snapped' : ''), style: posStyle() }, [
870
- titleBar,
871
- el('div', { key: 'body', className: 'xq-body' }, [
872
- el(XueqiuPanel, null)
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
- el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xueqiu",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "雪球 mini 行情面板 — DeepSeek Harness 免登录 A股/港美股实时行情、K线(蜡烛/成交量/均线)、分时图、热榜、搜索、7x24 快讯、热议用户。可拖拽悬浮面板,交易时段智能刷新。",
5
5
  "keywords": [
6
6
  "dsh",
@@ -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, minimized: false, tab: 'market', closed: false, hydrated: false,
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, tab: ui.tab, minimized: ui.minimized
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.snap === 'br') { ui.set({ snap: null }); return }
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 = { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
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
- const w = Math.min(Math.max(460, resizeRef.w + (e.clientX - resizeRef.x)), 1100)
837
- const h = Math.min(Math.max(380, resizeRef.h + (e.clientY - resizeRef.y)), 1000)
838
- ui.set({ size: { w: w, h: h }, snap: null })
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
- const minStyle = ui.pos ? { left: ui.pos.x, top: ui.pos.y, right: 'auto', bottom: 'auto' } : null
854
- return el('div', { className: 'xq-float xq-min', style: minStyle, ...dragProps, onClick: function () { ui.set({ minimized: false }) } }, [
855
- el('span', { key: 'logo', className: 'xq-logo' }, [el('b', { key: 'b' }, '雪球'), ' mini']),
856
- el('span', { key: 'sum', className: 'xq-sum' }, idxLine || '点击展开行情面板'),
857
- el('span', { key: 'sp', className: 'xq-spacer' }),
858
- el('button', { key: 'x', className: 'xq-btn-mini', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); ui.set({ closed: true }) } }, '✕'),
859
- el('span', { key: 'caret', className: 'xq-caret' }, '展开 ▾')
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: ui.snap === 'br' ? '解除右下角吸附' : '吸附到右下角', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snap === 'br' ? '📌' : '📍'),
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-float' + (ui.snap ? ' xq-snapped' : ''), style: posStyle() }, [
875
- titleBar,
876
- el('div', { key: 'body', className: 'xq-body' }, [
877
- el(XueqiuPanel, null)
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
- el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
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
  }