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 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,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, 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
 
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
- snapCheck()
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.snap === 'br') { ui.set({ snap: null }); return }
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 = { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
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
- 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 })
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
- 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' }, '展开 ▾')
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: ui.snap === 'br' ? '解除右下角吸附' : '吸附到右下角', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snap === 'br' ? '📌' : '📍'),
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-float' + (ui.snap ? ' xq-snapped' : ''), style: posStyle() }, [
870
- titleBar,
871
- el('div', { key: 'body', className: 'xq-body' }, [
872
- el(XueqiuPanel, null)
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
- el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xueqiu",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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,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, 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
 
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
- snapCheck()
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.snap === 'br') { ui.set({ snap: null }); return }
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 = { w: s.w, h: s.h, x: e.clientX, y: e.clientY }
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
- 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 })
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
- 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' }, '展开 ▾')
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: ui.snap === 'br' ? '解除右下角吸附' : '吸附到右下角', onPointerDown: function (e) { e.stopPropagation() }, onClick: function (e) { e.stopPropagation(); toggleSnap() } }, ui.snap === 'br' ? '📌' : '📍'),
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-float' + (ui.snap ? ' xq-snapped' : ''), style: posStyle() }, [
875
- titleBar,
876
- el('div', { key: 'body', className: 'xq-body' }, [
877
- el(XueqiuPanel, null)
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
- el('div', { key: 'rs', className: 'xq-resize', ...rsProps })
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
  }