@qzsy/dsh-bridge 3.3.0 → 3.4.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/CHANGELOG.md +31 -0
- package/README.md +3 -0
- package/client/client.js +540 -19
- package/client/mobile.js +570 -23
- package/package.json +2 -2
package/client/mobile.js
CHANGED
|
@@ -1,42 +1,79 @@
|
|
|
1
1
|
// 移动端抽屉导航:把宿主左侧栏(会话/工作区菜单)改成从左侧滑出的 sheet。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// 设计目标(对齐 tmp/dsh-bridge 的移动端体验,但只保留本项目需要的部分):
|
|
4
|
+
// - 窄屏内容占满屏宽,左侧栏变成带遮罩的抽屉;
|
|
5
|
+
// - 支持全面屏安全区(刘海/状态栏/home indicator),菜单按钮和抽屉内容都不被遮挡;
|
|
6
|
+
// - 支持手势:左缘右滑打开、抽屉内左滑关闭,拖动时抽屉跟随手指,松手吸附;
|
|
7
|
+
// - 桌面端完全不受影响。
|
|
6
8
|
//
|
|
7
9
|
// 宿主 DOM 是 CSS Modules 哈希类名,本模块只依赖稳定结构/ARIA:
|
|
8
10
|
// div[class*="_frame"] / _sidebarCol / _centerCol / _rightbarCol / _overlayLayer
|
|
9
11
|
// button[aria-label*="打开侧边栏"](英文 Open sidebar)
|
|
10
12
|
// div[class*="sessionRow"](会话行;projectRow 是工作区分组,不能误关)
|
|
11
|
-
// 不写死任何宿主哈希,宿主升级换前缀时选择器仍有效。
|
|
12
13
|
import { createTranslator } from './locales.mjs';
|
|
13
14
|
|
|
14
15
|
export const MOBILE_MAX_WIDTH = 767;
|
|
15
16
|
export const MOBILE_DRAWER_CSS_ID = 'qzsy-mobile-drawer-styles';
|
|
16
17
|
export const MOBILE_UI_FLAG = 'data-qzsy-mobile-drawer';
|
|
17
18
|
const OPEN_CLASS = 'qzsy-drawer-open';
|
|
19
|
+
const DRAGGING_CLASS = 'qzsy-drawer-dragging';
|
|
20
|
+
const HEADER_CLASS = 'qzsy-mobile-header';
|
|
18
21
|
const MENU_CLASS = 'qzsy-mobile-menu-btn';
|
|
19
22
|
const BACKDROP_CLASS = 'qzsy-mobile-backdrop';
|
|
20
23
|
const STYLE_PLUGIN = '@qzsy/dsh-bridge';
|
|
24
|
+
const EDGE_START_ZONE = 28;
|
|
25
|
+
const DRAG_THRESHOLD = 8;
|
|
21
26
|
|
|
22
27
|
export const MOBILE_DRAWER_CSS = `
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
:root {
|
|
29
|
+
--qzsy-safe-top: env(safe-area-inset-top, 0px);
|
|
30
|
+
--qzsy-safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
31
|
+
--qzsy-safe-left: env(safe-area-inset-left, 0px);
|
|
32
|
+
--qzsy-safe-right: env(safe-area-inset-right, 0px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.qzsy-mobile-header,
|
|
36
|
+
.qzsy-mobile-backdrop,
|
|
37
|
+
.qzsy-model-sheet-backdrop {
|
|
25
38
|
display: none;
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
/* 菜单按钮容器:始终在宿主之上;容器自身不拦截点击 */
|
|
42
|
+
.qzsy-mobile-header {
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 0;
|
|
45
|
+
left: 0;
|
|
46
|
+
right: 0;
|
|
47
|
+
z-index: 65;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
align-items: flex-end;
|
|
50
|
+
padding-top: 10px;
|
|
51
|
+
padding-top: calc(10px + var(--qzsy-safe-top, 0px));
|
|
52
|
+
padding-right: 10px;
|
|
53
|
+
padding-right: calc(10px + var(--qzsy-safe-right, 0px));
|
|
54
|
+
padding-bottom: 2px;
|
|
55
|
+
padding-left: 10px;
|
|
56
|
+
padding-left: calc(10px + var(--qzsy-safe-left, 0px));
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
@media (max-width: 767px) {
|
|
29
|
-
|
|
61
|
+
.qzsy-mobile-header { display: flex; }
|
|
62
|
+
|
|
63
|
+
/* 1. 左侧栏脱离 grid,内容占满;顶部给菜单按钮留出安全区高度 */
|
|
30
64
|
[data-slot="root"] > div[class*="_frame"] {
|
|
31
65
|
grid-template-columns: 0px minmax(0px, 1fr) minmax(0px, 0px) !important;
|
|
32
|
-
padding-top: 52px !important;
|
|
33
66
|
box-sizing: border-box !important;
|
|
67
|
+
padding-top: 52px !important;
|
|
68
|
+
padding-top: calc(52px + var(--qzsy-safe-top, 0px)) !important;
|
|
69
|
+
padding-bottom: 0 !important;
|
|
70
|
+
padding-bottom: var(--qzsy-safe-bottom, 0px) !important;
|
|
34
71
|
}
|
|
35
72
|
div[class*="_centerCol"] { grid-column: 2 !important; }
|
|
36
73
|
div[class*="_rightbarCol"] { grid-column: 3 !important; }
|
|
37
74
|
div[class*="_overlayLayer"] { grid-column: 1 / -1 !important; }
|
|
38
75
|
|
|
39
|
-
/* 2. 原生侧栏改为 fixed
|
|
76
|
+
/* 2. 原生侧栏改为 fixed 抽屉(默认藏在屏幕左侧;内部避开安全区) */
|
|
40
77
|
div[class*="_sidebarCol"] {
|
|
41
78
|
position: fixed !important;
|
|
42
79
|
top: 0 !important;
|
|
@@ -46,16 +83,26 @@ export const MOBILE_DRAWER_CSS = `
|
|
|
46
83
|
max-width: 84vw !important;
|
|
47
84
|
height: 100vh !important;
|
|
48
85
|
height: 100dvh !important;
|
|
86
|
+
box-sizing: border-box !important;
|
|
87
|
+
padding-top: 0 !important;
|
|
88
|
+
padding-top: var(--qzsy-safe-top, 0px) !important;
|
|
89
|
+
padding-bottom: 0 !important;
|
|
90
|
+
padding-bottom: var(--qzsy-safe-bottom, 0px) !important;
|
|
91
|
+
padding-left: 0 !important;
|
|
92
|
+
padding-left: var(--qzsy-safe-left, 0px) !important;
|
|
49
93
|
z-index: 60 !important;
|
|
50
94
|
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
51
95
|
border-right: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08)) !important;
|
|
52
96
|
box-shadow: none !important;
|
|
53
97
|
transform: translateX(-102%) !important;
|
|
54
|
-
transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
55
98
|
overflow: hidden !important;
|
|
56
99
|
overscroll-behavior: contain;
|
|
57
100
|
will-change: transform;
|
|
58
101
|
}
|
|
102
|
+
/* 首帧先就位、不播放入场动画,避免页面加载时侧栏从 0 滑出 */
|
|
103
|
+
body.qzsy-drawer-ready div[class*="_sidebarCol"] {
|
|
104
|
+
transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
105
|
+
}
|
|
59
106
|
body.qzsy-drawer-open div[class*="_sidebarCol"] {
|
|
60
107
|
transform: translateX(0) !important;
|
|
61
108
|
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.18) !important;
|
|
@@ -64,6 +111,17 @@ export const MOBILE_DRAWER_CSS = `
|
|
|
64
111
|
width: 100% !important;
|
|
65
112
|
}
|
|
66
113
|
|
|
114
|
+
/* 拖动中:关闭过渡,transform/遮罩直接跟手 */
|
|
115
|
+
body.qzsy-drawer-dragging div[class*="_sidebarCol"] {
|
|
116
|
+
transition: none !important;
|
|
117
|
+
transform: translateX(var(--qzsy-drawer-x, -102%)) !important;
|
|
118
|
+
}
|
|
119
|
+
body.qzsy-drawer-dragging .qzsy-mobile-backdrop {
|
|
120
|
+
transition: none !important;
|
|
121
|
+
opacity: var(--qzsy-drawer-progress, 0) !important;
|
|
122
|
+
pointer-events: auto !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
67
125
|
/* 3. 半透明遮罩:点击收起 */
|
|
68
126
|
.qzsy-mobile-backdrop {
|
|
69
127
|
display: block;
|
|
@@ -82,17 +140,13 @@ export const MOBILE_DRAWER_CSS = `
|
|
|
82
140
|
pointer-events: auto;
|
|
83
141
|
}
|
|
84
142
|
|
|
85
|
-
/* 4.
|
|
143
|
+
/* 4. 菜单按钮:位于安全区下方的 52px 留白条内 */
|
|
86
144
|
.qzsy-mobile-menu-btn {
|
|
87
145
|
display: inline-flex;
|
|
88
|
-
position: fixed;
|
|
89
|
-
top: 10px;
|
|
90
|
-
left: 10px;
|
|
91
|
-
width: 40px;
|
|
92
|
-
height: 40px;
|
|
93
|
-
z-index: 65;
|
|
94
146
|
align-items: center;
|
|
95
147
|
justify-content: center;
|
|
148
|
+
width: 40px;
|
|
149
|
+
height: 40px;
|
|
96
150
|
padding: 0;
|
|
97
151
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
98
152
|
border-radius: 12px;
|
|
@@ -100,6 +154,7 @@ export const MOBILE_DRAWER_CSS = `
|
|
|
100
154
|
color: var(--dsw-alias-label-primary, #111827);
|
|
101
155
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
|
102
156
|
cursor: pointer;
|
|
157
|
+
pointer-events: auto;
|
|
103
158
|
-webkit-tap-highlight-color: transparent;
|
|
104
159
|
transition: opacity 180ms ease, transform 180ms ease;
|
|
105
160
|
}
|
|
@@ -109,6 +164,109 @@ export const MOBILE_DRAWER_CSS = `
|
|
|
109
164
|
opacity: 0;
|
|
110
165
|
pointer-events: none;
|
|
111
166
|
}
|
|
167
|
+
|
|
168
|
+
/* 5. 模型切换器:把宿主桌面 popover 改成移动端底部 sheet */
|
|
169
|
+
@keyframes qzsy-model-sheet-in {
|
|
170
|
+
from { transform: translateY(100%); opacity: 0.6; }
|
|
171
|
+
to { transform: translateY(0); opacity: 1; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.qzsy-model-sheet-backdrop {
|
|
175
|
+
display: block;
|
|
176
|
+
position: fixed;
|
|
177
|
+
inset: 0;
|
|
178
|
+
z-index: 1190;
|
|
179
|
+
background: rgba(0, 0, 0, 0.4);
|
|
180
|
+
opacity: 0;
|
|
181
|
+
pointer-events: none;
|
|
182
|
+
transition: opacity 240ms ease;
|
|
183
|
+
touch-action: none;
|
|
184
|
+
-webkit-tap-highlight-color: transparent;
|
|
185
|
+
}
|
|
186
|
+
body.qzsy-model-sheet-open .qzsy-model-sheet-backdrop {
|
|
187
|
+
opacity: 1;
|
|
188
|
+
pointer-events: auto;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
[data-qzsy-model-sheet] {
|
|
192
|
+
position: fixed !important;
|
|
193
|
+
top: auto !important;
|
|
194
|
+
left: 0 !important;
|
|
195
|
+
right: 0 !important;
|
|
196
|
+
bottom: 0 !important;
|
|
197
|
+
width: 100vw !important;
|
|
198
|
+
max-width: 100vw !important;
|
|
199
|
+
height: auto !important;
|
|
200
|
+
max-height: 82vh !important;
|
|
201
|
+
max-height: 82dvh !important;
|
|
202
|
+
margin: 0 !important;
|
|
203
|
+
padding: 18px 0 0 !important;
|
|
204
|
+
border: 0 !important;
|
|
205
|
+
border-radius: 18px 18px 0 0 !important;
|
|
206
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
207
|
+
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.18) !important;
|
|
208
|
+
box-sizing: border-box !important;
|
|
209
|
+
display: flex !important;
|
|
210
|
+
flex-direction: column !important;
|
|
211
|
+
overflow: hidden !important;
|
|
212
|
+
z-index: 1200 !important;
|
|
213
|
+
transform-origin: bottom center !important;
|
|
214
|
+
animation: qzsy-model-sheet-in 260ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
215
|
+
will-change: transform;
|
|
216
|
+
}
|
|
217
|
+
[data-qzsy-model-sheet]::before {
|
|
218
|
+
content: '';
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 7px;
|
|
221
|
+
left: 50%;
|
|
222
|
+
width: 36px;
|
|
223
|
+
height: 4px;
|
|
224
|
+
margin-left: -18px;
|
|
225
|
+
border-radius: 999px;
|
|
226
|
+
background: rgba(0, 0, 0, 0.18);
|
|
227
|
+
pointer-events: none;
|
|
228
|
+
}
|
|
229
|
+
[data-qzsy-model-sheet] > div,
|
|
230
|
+
[data-qzsy-model-sheet] div[role="menu"] {
|
|
231
|
+
width: 100% !important;
|
|
232
|
+
max-width: 100% !important;
|
|
233
|
+
border: 0 !important;
|
|
234
|
+
border-radius: 0 !important;
|
|
235
|
+
background: transparent !important;
|
|
236
|
+
box-shadow: none !important;
|
|
237
|
+
box-sizing: border-box !important;
|
|
238
|
+
}
|
|
239
|
+
[data-qzsy-model-sheet] div[role="menu"] {
|
|
240
|
+
max-height: calc(82vh - 40px) !important;
|
|
241
|
+
max-height: calc(82dvh - 40px) !important;
|
|
242
|
+
overflow-y: auto !important;
|
|
243
|
+
overscroll-behavior: contain !important;
|
|
244
|
+
padding: 8px 12px calc(14px + var(--qzsy-safe-bottom, 0px)) !important;
|
|
245
|
+
}
|
|
246
|
+
[data-qzsy-model-sheet] input {
|
|
247
|
+
width: 100% !important;
|
|
248
|
+
box-sizing: border-box !important;
|
|
249
|
+
}
|
|
250
|
+
[data-qzsy-model-sheet] button[role="menuitem"],
|
|
251
|
+
[data-qzsy-model-sheet] [role="option"] {
|
|
252
|
+
width: 100% !important;
|
|
253
|
+
box-sizing: border-box !important;
|
|
254
|
+
}
|
|
255
|
+
[data-qzsy-model-sheet].qzsy-model-sheet-closing {
|
|
256
|
+
transform: translateY(100%) !important;
|
|
257
|
+
opacity: 0 !important;
|
|
258
|
+
transition: transform 220ms ease, opacity 220ms ease !important;
|
|
259
|
+
animation: none !important;
|
|
260
|
+
}
|
|
261
|
+
body.qzsy-model-sheet-dragging [data-qzsy-model-sheet] {
|
|
262
|
+
transform: translateY(var(--qzsy-model-sheet-y, 0px)) !important;
|
|
263
|
+
transition: none !important;
|
|
264
|
+
animation: none !important;
|
|
265
|
+
}
|
|
266
|
+
body.qzsy-model-sheet-dragging .qzsy-model-sheet-backdrop {
|
|
267
|
+
opacity: var(--qzsy-model-sheet-progress, 1) !important;
|
|
268
|
+
transition: none !important;
|
|
269
|
+
}
|
|
112
270
|
}
|
|
113
271
|
`;
|
|
114
272
|
|
|
@@ -137,6 +295,11 @@ function injectStyles() {
|
|
|
137
295
|
if (style.textContent !== MOBILE_DRAWER_CSS) style.textContent = MOBILE_DRAWER_CSS;
|
|
138
296
|
}
|
|
139
297
|
|
|
298
|
+
function drawerWidth() {
|
|
299
|
+
if (typeof window === 'undefined') return 300;
|
|
300
|
+
return Math.min(300, Math.round(window.innerWidth * 0.84));
|
|
301
|
+
}
|
|
302
|
+
|
|
140
303
|
/**
|
|
141
304
|
* 安装移动端抽屉导航。无 DOM 环境(单测/SSR)安全退化为 no-op。
|
|
142
305
|
* 返回 disposer;宿主若支持 ctx.effect,可注册为插件副作用。
|
|
@@ -150,18 +313,31 @@ export function setupMobileDrawer() {
|
|
|
150
313
|
let menuBtn = null;
|
|
151
314
|
let observer = null;
|
|
152
315
|
let disposed = false;
|
|
316
|
+
let gesture = null;
|
|
317
|
+
let suppressClickUntil = 0;
|
|
318
|
+
let modelPortal = null;
|
|
319
|
+
let modelBackdrop = null;
|
|
320
|
+
let modelChainUntil = 0;
|
|
321
|
+
let modelDrag = null;
|
|
322
|
+
let modelDragHandlers = null;
|
|
153
323
|
|
|
154
324
|
const ensureElements = () => {
|
|
155
325
|
if (disposed || !document.body) return;
|
|
156
326
|
injectStyles();
|
|
157
327
|
|
|
158
|
-
let
|
|
328
|
+
let header = document.querySelector(`.${HEADER_CLASS}`);
|
|
329
|
+
if (!header) {
|
|
330
|
+
header = document.createElement('div');
|
|
331
|
+
header.className = HEADER_CLASS;
|
|
332
|
+
document.body.appendChild(header);
|
|
333
|
+
}
|
|
334
|
+
let btn = header.querySelector(`.${MENU_CLASS}`);
|
|
159
335
|
if (!btn) {
|
|
160
336
|
btn = document.createElement('button');
|
|
161
337
|
btn.type = 'button';
|
|
162
338
|
btn.className = MENU_CLASS;
|
|
163
339
|
btn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="8" x2="21" y2="8"></line><line x1="3" y1="15" x2="15" y2="15"></line></svg>';
|
|
164
|
-
|
|
340
|
+
header.appendChild(btn);
|
|
165
341
|
}
|
|
166
342
|
let mask = document.querySelector(`.${BACKDROP_CLASS}`);
|
|
167
343
|
if (!mask) {
|
|
@@ -194,6 +370,17 @@ export function setupMobileDrawer() {
|
|
|
194
370
|
}
|
|
195
371
|
};
|
|
196
372
|
|
|
373
|
+
const setDragProgress = (x, width) => {
|
|
374
|
+
document.body.style.setProperty('--qzsy-drawer-x', `${x}px`);
|
|
375
|
+
document.body.style.setProperty('--qzsy-drawer-progress', String(Math.max(0, Math.min(1, 1 - Math.abs(x) / width))));
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const clearDrag = () => {
|
|
379
|
+
document.body.classList.remove(DRAGGING_CLASS);
|
|
380
|
+
document.body.style.removeProperty('--qzsy-drawer-x');
|
|
381
|
+
document.body.style.removeProperty('--qzsy-drawer-progress');
|
|
382
|
+
};
|
|
383
|
+
|
|
197
384
|
const openDrawer = () => {
|
|
198
385
|
if (disposed || !isMobileViewport() || !document.body) return;
|
|
199
386
|
ensureElements();
|
|
@@ -218,7 +405,234 @@ export function setupMobileDrawer() {
|
|
|
218
405
|
}, 60);
|
|
219
406
|
};
|
|
220
407
|
|
|
408
|
+
// ---- 模型切换器:宿主桌面 popover → 移动端底部 sheet ----
|
|
409
|
+
const bodyPortalOf = (node) => {
|
|
410
|
+
let el = node;
|
|
411
|
+
while (el && el.parentElement && el.parentElement !== document.body) el = el.parentElement;
|
|
412
|
+
return el && el.parentElement === document.body ? el : null;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const visiblePortal = (el) => {
|
|
416
|
+
if (!el || !el.isConnected) return false;
|
|
417
|
+
const s = getComputedStyle(el);
|
|
418
|
+
if (s.display === 'none' || s.visibility === 'hidden') return false;
|
|
419
|
+
const r = el.getBoundingClientRect();
|
|
420
|
+
return r.width > 0 && r.height > 0;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const isModelTrigger = (target) => {
|
|
424
|
+
const el = target?.closest?.('button, [role="menuitem"], [role="button"], a');
|
|
425
|
+
if (!el) return false;
|
|
426
|
+
const label = `${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''} ${el.innerText || ''}`;
|
|
427
|
+
return /模型|Model|model/.test(label);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const findModelPortal = () => {
|
|
431
|
+
if (!isMobileViewport()) return null;
|
|
432
|
+
const search = document.querySelector(
|
|
433
|
+
'input[placeholder*="搜索模型"], input[placeholder*="Search model"], input[aria-label*="搜索模型"], input[aria-label*="Search model"]',
|
|
434
|
+
);
|
|
435
|
+
if (search) {
|
|
436
|
+
const menu = search.closest('[role="menu"]') || search.closest('[class*="_menu"]');
|
|
437
|
+
const portal = bodyPortalOf(menu || search);
|
|
438
|
+
if (portal && visiblePortal(portal)) {
|
|
439
|
+
const s = getComputedStyle(portal);
|
|
440
|
+
if ((s.position === 'fixed' || s.position === 'absolute') && portal.getBoundingClientRect().width > 120) return portal;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// 无搜索框的宿主(如移动端版本)用「模型触发链 + 菜单行数」兜底
|
|
444
|
+
if (Date.now() > modelChainUntil) return null;
|
|
445
|
+
const candidates = [];
|
|
446
|
+
for (const el of document.body.children) {
|
|
447
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
448
|
+
if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
|
|
449
|
+
if (!visiblePortal(el)) continue;
|
|
450
|
+
const s = getComputedStyle(el);
|
|
451
|
+
if (s.position !== 'fixed' && s.position !== 'absolute') continue;
|
|
452
|
+
const menu = el.querySelector('[role="menu"], [role="listbox"]');
|
|
453
|
+
if (!menu) continue;
|
|
454
|
+
const text = menu.innerText || '';
|
|
455
|
+
const rows = menu.querySelectorAll('[role="menuitem"], [role="option"], button, a, [tabindex]').length;
|
|
456
|
+
const hasSearch = Boolean(el.querySelector('input[placeholder*="搜索模型"], input[placeholder*="Search model"]'));
|
|
457
|
+
// 排除「模型 / 推理等级」两行的第一层 popover
|
|
458
|
+
if (/推理等级|Reasoning effort|推理强度/i.test(text) && rows <= 2) continue;
|
|
459
|
+
// 已经是全宽底部 sheet 的宿主菜单不再重复改造
|
|
460
|
+
const rect = el.getBoundingClientRect();
|
|
461
|
+
const alreadySheet = rect.width >= window.innerWidth - 2 && Math.abs(rect.bottom - window.innerHeight) < 2 && rect.height > 120;
|
|
462
|
+
if (alreadySheet && !hasSearch) continue;
|
|
463
|
+
if (hasSearch || rows > 2) candidates.push({ el, rows, hasSearch });
|
|
464
|
+
}
|
|
465
|
+
candidates.sort((a, b) => Number(b.hasSearch) - Number(a.hasSearch) || b.rows - a.rows);
|
|
466
|
+
return candidates[0]?.el || null;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const hostBackdropPresent = () => {
|
|
470
|
+
for (const el of document.body.children) {
|
|
471
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
472
|
+
if (el === modelBackdrop || el.classList.contains(HEADER_CLASS) || el.classList.contains(BACKDROP_CLASS)) continue;
|
|
473
|
+
const s = getComputedStyle(el);
|
|
474
|
+
if (s.position !== 'fixed') continue;
|
|
475
|
+
const r = el.getBoundingClientRect();
|
|
476
|
+
if (r.width < window.innerWidth - 4 || r.height < window.innerHeight - 4) continue;
|
|
477
|
+
if (Number(s.opacity) < 0.1) continue;
|
|
478
|
+
const bg = s.backgroundColor || '';
|
|
479
|
+
if (!/^rgba?\(/.test(bg) || /rgba\(0,\s*0,\s*0,\s*0\)/.test(bg)) continue;
|
|
480
|
+
if (el.querySelector('[role="menu"]')) continue;
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const requestHostModelClose = (portal) => {
|
|
487
|
+
if (disposed || !portal || !portal.isConnected) return;
|
|
488
|
+
const PointerCtor = typeof window.PointerEvent === 'function' ? window.PointerEvent : window.MouseEvent;
|
|
489
|
+
try { document.body.dispatchEvent(new PointerCtor('pointerdown', { bubbles: true, cancelable: true })); } catch { /* 忽略 */ }
|
|
490
|
+
try { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); } catch { /* 忽略 */ }
|
|
491
|
+
window.setTimeout(() => {
|
|
492
|
+
if (disposed || !portal.isConnected) return;
|
|
493
|
+
const trigger = document.querySelector('button[aria-haspopup="menu"][aria-expanded="true"][aria-label*="选择模型"], button[aria-haspopup="menu"][aria-expanded="true"][aria-label*="Select model"]');
|
|
494
|
+
try { trigger?.click(); } catch { /* 忽略 */ }
|
|
495
|
+
window.setTimeout(() => {
|
|
496
|
+
if (!disposed && portal.isConnected && modelPortal === portal) clearModelSheet();
|
|
497
|
+
}, 450);
|
|
498
|
+
}, 120);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const closeModelSheetNow = () => {
|
|
502
|
+
const portal = modelPortal;
|
|
503
|
+
if (!portal) return;
|
|
504
|
+
document.body.classList.remove('qzsy-model-sheet-open');
|
|
505
|
+
document.body.style.removeProperty('--qzsy-model-sheet-y');
|
|
506
|
+
document.body.style.removeProperty('--qzsy-model-sheet-progress');
|
|
507
|
+
requestHostModelClose(portal);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const closeModelSheetAnimated = () => {
|
|
511
|
+
const portal = modelPortal;
|
|
512
|
+
if (!portal) return;
|
|
513
|
+
portal.classList.add('qzsy-model-sheet-closing');
|
|
514
|
+
document.body.classList.remove('qzsy-model-sheet-open');
|
|
515
|
+
window.setTimeout(() => requestHostModelClose(portal), 180);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const ensureModelBackdrop = () => {
|
|
519
|
+
if (modelBackdrop || hostBackdropPresent()) return;
|
|
520
|
+
modelBackdrop = document.createElement('div');
|
|
521
|
+
modelBackdrop.className = 'qzsy-model-sheet-backdrop';
|
|
522
|
+
modelBackdrop.setAttribute('aria-hidden', 'true');
|
|
523
|
+
modelBackdrop.addEventListener('pointerdown', (event) => event.stopPropagation(), true);
|
|
524
|
+
modelBackdrop.addEventListener('click', (event) => {
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
event.stopPropagation();
|
|
527
|
+
closeModelSheetNow();
|
|
528
|
+
});
|
|
529
|
+
document.body.appendChild(modelBackdrop);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const detachModelDrag = () => {
|
|
533
|
+
if (!modelPortal || !modelDragHandlers) return;
|
|
534
|
+
modelPortal.removeEventListener('touchstart', modelDragHandlers.onStart);
|
|
535
|
+
modelPortal.removeEventListener('touchmove', modelDragHandlers.onMove);
|
|
536
|
+
modelPortal.removeEventListener('touchend', modelDragHandlers.onEnd);
|
|
537
|
+
modelPortal.removeEventListener('touchcancel', modelDragHandlers.onEnd);
|
|
538
|
+
modelDragHandlers = null;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const attachModelDrag = (portal) => {
|
|
542
|
+
if (!portal || modelDragHandlers) return;
|
|
543
|
+
const onStart = (event) => {
|
|
544
|
+
if (event.touches?.length !== 1) return;
|
|
545
|
+
const touch = event.touches[0];
|
|
546
|
+
const rect = portal.getBoundingClientRect();
|
|
547
|
+
if (touch.clientY - rect.top > 30) return;
|
|
548
|
+
if (event.target.closest?.('input, textarea, select, button, a, [contenteditable="true"]')) return;
|
|
549
|
+
modelDrag = { startY: touch.clientY, startT: Date.now(), dy: 0, active: false, height: rect.height };
|
|
550
|
+
};
|
|
551
|
+
const onMove = (event) => {
|
|
552
|
+
if (!modelDrag) return;
|
|
553
|
+
const touch = event.touches?.[0];
|
|
554
|
+
if (!touch) return;
|
|
555
|
+
const dy = touch.clientY - modelDrag.startY;
|
|
556
|
+
if (!modelDrag.active) {
|
|
557
|
+
if (dy < 6) return;
|
|
558
|
+
modelDrag.active = true;
|
|
559
|
+
document.body.classList.add('qzsy-model-sheet-dragging');
|
|
560
|
+
}
|
|
561
|
+
modelDrag.dy = Math.max(0, dy);
|
|
562
|
+
document.body.style.setProperty('--qzsy-model-sheet-y', `${modelDrag.dy}px`);
|
|
563
|
+
document.body.style.setProperty('--qzsy-model-sheet-progress', String(Math.max(0, 1 - modelDrag.dy / Math.max(1, modelDrag.height))));
|
|
564
|
+
if (event.cancelable) event.preventDefault();
|
|
565
|
+
};
|
|
566
|
+
const onEnd = () => {
|
|
567
|
+
if (!modelDrag) return;
|
|
568
|
+
const drag = modelDrag;
|
|
569
|
+
modelDrag = null;
|
|
570
|
+
if (!drag.active) return;
|
|
571
|
+
document.body.classList.remove('qzsy-model-sheet-dragging');
|
|
572
|
+
document.body.style.removeProperty('--qzsy-model-sheet-y');
|
|
573
|
+
document.body.style.removeProperty('--qzsy-model-sheet-progress');
|
|
574
|
+
const velocity = drag.dy / Math.max(1, Date.now() - drag.startT);
|
|
575
|
+
if (drag.dy > 110 || velocity > 0.5) closeModelSheetAnimated();
|
|
576
|
+
};
|
|
577
|
+
portal.addEventListener('touchstart', onStart, { passive: true });
|
|
578
|
+
portal.addEventListener('touchmove', onMove, { passive: false });
|
|
579
|
+
portal.addEventListener('touchend', onEnd, { passive: true });
|
|
580
|
+
portal.addEventListener('touchcancel', onEnd, { passive: true });
|
|
581
|
+
modelDragHandlers = { onStart, onMove, onEnd };
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const clearModelSheet = () => {
|
|
585
|
+
if (modelPortal) {
|
|
586
|
+
detachModelDrag();
|
|
587
|
+
modelPortal.removeAttribute('data-qzsy-model-sheet');
|
|
588
|
+
modelPortal.classList.remove('qzsy-model-sheet', 'qzsy-model-sheet-closing');
|
|
589
|
+
}
|
|
590
|
+
modelPortal = null;
|
|
591
|
+
modelDrag = null;
|
|
592
|
+
document.body?.classList.remove('qzsy-model-sheet-open', 'qzsy-model-sheet-dragging');
|
|
593
|
+
document.body?.style.removeProperty('--qzsy-model-sheet-y');
|
|
594
|
+
document.body?.style.removeProperty('--qzsy-model-sheet-progress');
|
|
595
|
+
if (modelBackdrop) {
|
|
596
|
+
modelBackdrop.remove();
|
|
597
|
+
modelBackdrop = null;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const applyModelSheet = (portal) => {
|
|
602
|
+
if (modelPortal === portal) return;
|
|
603
|
+
clearModelSheet();
|
|
604
|
+
modelPortal = portal;
|
|
605
|
+
portal.setAttribute('data-qzsy-model-sheet', '');
|
|
606
|
+
portal.classList.add('qzsy-model-sheet');
|
|
607
|
+
attachModelDrag(portal);
|
|
608
|
+
ensureModelBackdrop();
|
|
609
|
+
window.requestAnimationFrame(() => {
|
|
610
|
+
if (!disposed && modelPortal === portal) document.body.classList.add('qzsy-model-sheet-open');
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const syncModelSheet = () => {
|
|
615
|
+
if (disposed) return;
|
|
616
|
+
if (!isMobileViewport()) {
|
|
617
|
+
clearModelSheet();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (modelPortal && !visiblePortal(modelPortal)) clearModelSheet();
|
|
621
|
+
const found = findModelPortal();
|
|
622
|
+
if (!found) {
|
|
623
|
+
if (modelPortal?.isConnected) return; // 已应用,等待宿主关闭后移除
|
|
624
|
+
if (modelPortal) clearModelSheet();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (found !== modelPortal) applyModelSheet(found);
|
|
628
|
+
};
|
|
629
|
+
|
|
221
630
|
const onClick = (event) => {
|
|
631
|
+
if (Date.now() < suppressClickUntil) {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
event.stopPropagation();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
222
636
|
const target = event.target;
|
|
223
637
|
if (!isElement(target)) return;
|
|
224
638
|
|
|
@@ -228,6 +642,14 @@ export function setupMobileDrawer() {
|
|
|
228
642
|
else openDrawer();
|
|
229
643
|
return;
|
|
230
644
|
}
|
|
645
|
+
|
|
646
|
+
// 模型切换器:记录触发链,等宿主把二级菜单 portal 挂到 body 后再改造成 sheet
|
|
647
|
+
if (isModelTrigger(target)) {
|
|
648
|
+
modelChainUntil = Date.now() + 6000;
|
|
649
|
+
window.setTimeout(syncModelSheet, 30);
|
|
650
|
+
window.setTimeout(syncModelSheet, 180);
|
|
651
|
+
}
|
|
652
|
+
|
|
231
653
|
if (!isOpen()) return;
|
|
232
654
|
|
|
233
655
|
if (target.closest(`.${BACKDROP_CLASS}`)) {
|
|
@@ -265,11 +687,117 @@ export function setupMobileDrawer() {
|
|
|
265
687
|
};
|
|
266
688
|
|
|
267
689
|
const onResize = () => {
|
|
268
|
-
if (!isMobileViewport() && isOpen())
|
|
690
|
+
if (!isMobileViewport() && isOpen()) {
|
|
691
|
+
gesture = null;
|
|
692
|
+
clearDrag();
|
|
693
|
+
closeDrawer({ restoreFocus: false });
|
|
694
|
+
}
|
|
695
|
+
syncModelSheet();
|
|
269
696
|
};
|
|
270
697
|
|
|
271
698
|
const onVisibilityChange = () => {
|
|
272
|
-
if (document.visibilityState === 'hidden' && isOpen())
|
|
699
|
+
if (document.visibilityState === 'hidden' && isOpen()) {
|
|
700
|
+
gesture = null;
|
|
701
|
+
clearDrag();
|
|
702
|
+
closeDrawer({ restoreFocus: false });
|
|
703
|
+
}
|
|
704
|
+
if (document.visibilityState === 'visible') syncModelSheet();
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// ---- 手势:左缘右滑打开;抽屉内左滑关闭;拖动跟手 ----
|
|
708
|
+
const onTouchStart = (event) => {
|
|
709
|
+
if (modelPortal || disposed || !isMobileViewport() || event.touches?.length !== 1) return;
|
|
710
|
+
const touch = event.touches[0];
|
|
711
|
+
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
712
|
+
if (!sidebar) return;
|
|
713
|
+
const open = isOpen();
|
|
714
|
+
|
|
715
|
+
if (!open) {
|
|
716
|
+
if (touch.clientX > EDGE_START_ZONE) return;
|
|
717
|
+
// 先把宿主侧栏展开,保证拖动时抽屉里有完整会话列表
|
|
718
|
+
ensureSidebarExpanded();
|
|
719
|
+
gesture = {
|
|
720
|
+
mode: 'open',
|
|
721
|
+
startX: touch.clientX,
|
|
722
|
+
startY: touch.clientY,
|
|
723
|
+
startT: Date.now(),
|
|
724
|
+
width: drawerWidth(),
|
|
725
|
+
active: false,
|
|
726
|
+
dx: 0,
|
|
727
|
+
};
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!sidebar.contains(event.target)) return;
|
|
732
|
+
if (event.target.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
733
|
+
gesture = {
|
|
734
|
+
mode: 'close',
|
|
735
|
+
startX: touch.clientX,
|
|
736
|
+
startY: touch.clientY,
|
|
737
|
+
startT: Date.now(),
|
|
738
|
+
width: drawerWidth(),
|
|
739
|
+
active: false,
|
|
740
|
+
dx: 0,
|
|
741
|
+
};
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const onTouchMove = (event) => {
|
|
745
|
+
if (modelPortal || !gesture || disposed) return;
|
|
746
|
+
const touch = event.touches?.[0];
|
|
747
|
+
if (!touch) return;
|
|
748
|
+
const dx = touch.clientX - gesture.startX;
|
|
749
|
+
const dy = touch.clientY - gesture.startY;
|
|
750
|
+
|
|
751
|
+
if (!gesture.active) {
|
|
752
|
+
if (Math.abs(dx) < DRAG_THRESHOLD) return;
|
|
753
|
+
// 纵向为主:交还给宿主滚动,本次不拦截
|
|
754
|
+
if (Math.abs(dx) < Math.abs(dy) * 1.2) { gesture = null; return; }
|
|
755
|
+
if (gesture.mode === 'open' && dx <= 0) { gesture = null; return; }
|
|
756
|
+
if (gesture.mode === 'close' && dx >= 0) { gesture = null; return; }
|
|
757
|
+
gesture.active = true;
|
|
758
|
+
suppressClickUntil = Date.now() + 450;
|
|
759
|
+
document.body.classList.add(DRAGGING_CLASS);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const width = Math.max(1, gesture.width);
|
|
763
|
+
let x;
|
|
764
|
+
if (gesture.mode === 'open') x = Math.max(-width, Math.min(0, -width + dx));
|
|
765
|
+
else x = Math.max(-width, Math.min(0, dx));
|
|
766
|
+
gesture.dx = dx;
|
|
767
|
+
setDragProgress(x, width);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const finishGesture = (touch) => {
|
|
771
|
+
if (!gesture) return;
|
|
772
|
+
const current = gesture;
|
|
773
|
+
gesture = null;
|
|
774
|
+
if (!current.active) return;
|
|
775
|
+
|
|
776
|
+
const endX = touch ? touch.clientX : current.startX + current.dx;
|
|
777
|
+
const dx = endX - current.startX;
|
|
778
|
+
const elapsed = Math.max(1, Date.now() - current.startT);
|
|
779
|
+
const velocity = dx / elapsed;
|
|
780
|
+
const threshold = Math.min(90, Math.max(60, current.width * 0.3));
|
|
781
|
+
const openNow = current.mode === 'open'
|
|
782
|
+
? dx > threshold || velocity > 0.45
|
|
783
|
+
: !(dx < -Math.max(50, threshold * 0.7) || velocity < -0.35);
|
|
784
|
+
|
|
785
|
+
document.body.classList.toggle(OPEN_CLASS, openNow);
|
|
786
|
+
if (openNow) ensureSidebarExpanded();
|
|
787
|
+
syncAria();
|
|
788
|
+
suppressClickUntil = Date.now() + 250;
|
|
789
|
+
// 等一帧让浏览器接住当前拖动位置,再撤掉拖动样式做吸附动画
|
|
790
|
+
window.requestAnimationFrame(clearDrag);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const onTouchEnd = (event) => {
|
|
794
|
+
if (modelPortal || !gesture) return;
|
|
795
|
+
finishGesture(event.changedTouches?.[0]);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const onTouchCancel = () => {
|
|
799
|
+
if (modelPortal || !gesture) return;
|
|
800
|
+
finishGesture(null);
|
|
273
801
|
};
|
|
274
802
|
|
|
275
803
|
injectStyles();
|
|
@@ -278,27 +806,46 @@ export function setupMobileDrawer() {
|
|
|
278
806
|
document.addEventListener('keydown', onKeyDown, true);
|
|
279
807
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
280
808
|
window.addEventListener('resize', onResize);
|
|
809
|
+
window.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
810
|
+
window.addEventListener('touchmove', onTouchMove, { passive: true });
|
|
811
|
+
window.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
812
|
+
window.addEventListener('touchcancel', onTouchCancel, { passive: true });
|
|
813
|
+
|
|
814
|
+
// 等侧栏先无动画地藏到屏幕外,再启用过渡(否则首次加载会看到 280ms 滑出)
|
|
815
|
+
window.requestAnimationFrame(() => {
|
|
816
|
+
if (!disposed && document.body) document.body.classList.add('qzsy-drawer-ready');
|
|
817
|
+
});
|
|
281
818
|
|
|
282
819
|
if (typeof MutationObserver === 'function' && document.body) {
|
|
283
820
|
observer = new MutationObserver(() => {
|
|
284
|
-
if (!document.querySelector(`.${MENU_CLASS}`) || !document.querySelector(`.${BACKDROP_CLASS}`) || !document.getElementById(MOBILE_DRAWER_CSS_ID)) {
|
|
821
|
+
if (!document.querySelector(`.${HEADER_CLASS}`) || !document.querySelector(`.${MENU_CLASS}`) || !document.querySelector(`.${BACKDROP_CLASS}`) || !document.getElementById(MOBILE_DRAWER_CSS_ID)) {
|
|
285
822
|
ensureElements();
|
|
286
823
|
}
|
|
824
|
+
syncModelSheet();
|
|
287
825
|
});
|
|
288
826
|
observer.observe(document.body, { childList: true });
|
|
289
827
|
}
|
|
290
828
|
|
|
829
|
+
syncModelSheet();
|
|
291
830
|
syncAria();
|
|
292
831
|
|
|
293
832
|
return () => {
|
|
294
833
|
disposed = true;
|
|
834
|
+
gesture = null;
|
|
295
835
|
try { observer?.disconnect(); } catch { /* 忽略 */ }
|
|
296
836
|
document.removeEventListener('click', onClick, true);
|
|
297
837
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
298
838
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
299
839
|
window.removeEventListener('resize', onResize);
|
|
840
|
+
window.removeEventListener('touchstart', onTouchStart);
|
|
841
|
+
window.removeEventListener('touchmove', onTouchMove);
|
|
842
|
+
window.removeEventListener('touchend', onTouchEnd);
|
|
843
|
+
window.removeEventListener('touchcancel', onTouchCancel);
|
|
844
|
+
clearModelSheet();
|
|
845
|
+
clearDrag();
|
|
300
846
|
document.body?.classList.remove(OPEN_CLASS);
|
|
301
|
-
document.
|
|
847
|
+
document.body?.classList.remove('qzsy-drawer-ready');
|
|
848
|
+
document.querySelector(`.${HEADER_CLASS}`)?.remove();
|
|
302
849
|
document.querySelector(`.${BACKDROP_CLASS}`)?.remove();
|
|
303
850
|
document.getElementById(MOBILE_DRAWER_CSS_ID)?.remove();
|
|
304
851
|
root.removeAttribute(MOBILE_UI_FLAG);
|