@opentiny/next-sdk 0.4.2 → 0.4.3

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.
Files changed (88) hide show
  1. package/dist/{SimulatorMask-BHVXyogh-CARX3Rff.js → SimulatorMask-BHVXyogh-CKmEDsYG.js} +1 -1
  2. package/dist/{index-R_HIbfUX.js → index-CHMa2zG4.js} +1065 -1042
  3. package/dist/index.js +1 -1
  4. package/dist/page-tools/a11y/config.d.ts +6 -3
  5. package/dist/page-tools/a11y/constants.d.ts +20 -0
  6. package/dist/page-tools/a11y/utils.d.ts +14 -0
  7. package/dist/page-tools/constants.d.ts +4 -2
  8. package/dist/page-tools/context.d.ts +2 -0
  9. package/dist/page-tools/handlers/hover.d.ts +9 -0
  10. package/dist/page-tools/schema.d.ts +3 -3
  11. package/dist/page-tools/utils/dom.d.ts +8 -2
  12. package/dist/runtime.js +19 -172
  13. package/package.json +6 -1
  14. package/WebMcpClient.ts +0 -505
  15. package/WebMcpServer.ts +0 -447
  16. package/agent/AgentModelProvider.ts +0 -932
  17. package/agent/type.ts +0 -91
  18. package/agent/utils/generateReActPrompt.ts +0 -71
  19. package/agent/utils/getAISDKTools.ts +0 -34
  20. package/agent/utils/getBuiltinMcpTools.ts +0 -86
  21. package/agent/utils/parseReActAction.ts +0 -34
  22. package/core.ts +0 -26
  23. package/index.ts +0 -91
  24. package/page-tools/a11y/build.ts +0 -74
  25. package/page-tools/a11y/config.ts +0 -465
  26. package/page-tools/a11y/constants.ts +0 -131
  27. package/page-tools/a11y/search.ts +0 -127
  28. package/page-tools/a11y/types.ts +0 -105
  29. package/page-tools/a11y/utils.ts +0 -239
  30. package/page-tools/a11y/vnode.ts +0 -439
  31. package/page-tools/a11y-tree.ts +0 -14
  32. package/page-tools/bridge.ts +0 -722
  33. package/page-tools/configs/console-cloud.ts +0 -172
  34. package/page-tools/constants.ts +0 -12
  35. package/page-tools/context.ts +0 -50
  36. package/page-tools/effects.ts +0 -345
  37. package/page-tools/handlers/browserState.ts +0 -12
  38. package/page-tools/handlers/click.ts +0 -30
  39. package/page-tools/handlers/executeJavascript.ts +0 -22
  40. package/page-tools/handlers/fill.ts +0 -65
  41. package/page-tools/handlers/scroll.ts +0 -66
  42. package/page-tools/handlers/searchTree.ts +0 -27
  43. package/page-tools/handlers/select.ts +0 -39
  44. package/page-tools/initialize-builtin-WebMCP.ts +0 -20
  45. package/page-tools/page-agent-highlight/index.ts +0 -245
  46. package/page-tools/page-agent-mask/SimulatorMask.module.css +0 -14
  47. package/page-tools/page-agent-mask/SimulatorMask.ts +0 -299
  48. package/page-tools/page-agent-mask/checkDarkMode.ts +0 -181
  49. package/page-tools/page-agent-mask/cursor-border.svg +0 -3
  50. package/page-tools/page-agent-mask/cursor-fill.svg +0 -5
  51. package/page-tools/page-agent-mask/cursor.module.css +0 -70
  52. package/page-tools/page-agent-mask/hauwei.svg +0 -25
  53. package/page-tools/page-agent-prompt.md +0 -139
  54. package/page-tools/page-agent-tool-event.ts +0 -113
  55. package/page-tools/page-agent-tool.ts +0 -207
  56. package/page-tools/page-state-cache.ts +0 -78
  57. package/page-tools/schema.ts +0 -52
  58. package/page-tools/tool-config.ts +0 -100
  59. package/page-tools/utils/dom.ts +0 -158
  60. package/page-tools/utils/scroll.ts +0 -58
  61. package/remoter/QrCode.ts +0 -54
  62. package/remoter/createRemoter.ts +0 -957
  63. package/remoter/svgs/chat.svg +0 -43
  64. package/remoter/svgs/icon-copy.svg +0 -25
  65. package/remoter/svgs/link.svg +0 -28
  66. package/remoter/svgs/logo.svg +0 -45
  67. package/remoter/svgs/qrcode.svg +0 -35
  68. package/remoter/svgs/scan.svg +0 -21
  69. package/remoter/tooltips.ts +0 -260
  70. package/runtime.html +0 -39
  71. package/runtime.ts +0 -44
  72. package/script/utils.ts +0 -26
  73. package/skills/index.ts +0 -287
  74. package/test/page-tools/a11y/build.test.ts +0 -638
  75. package/test/page-tools/a11y/config.test.ts +0 -370
  76. package/test/page-tools/configs/console-cloud.test.ts +0 -168
  77. package/test/page-tools/page-agent-highlight.test.ts +0 -110
  78. package/test/page-tools/page-agent-tool-dispatch.test.ts +0 -208
  79. package/test/page-tools/page-agent-tool.test.ts +0 -102
  80. package/test/page-tools/tool-config.test.ts +0 -112
  81. package/test/page-tools/utils/dom.test.ts +0 -122
  82. package/tsconfig.json +0 -16
  83. package/utils/builtinProxy.ts +0 -144
  84. package/utils/env.ts +0 -13
  85. package/utils/uuid.ts +0 -10
  86. package/vite.config.runtime.ts +0 -22
  87. package/vite.config.ts +0 -63
  88. package/vitest.config.ts +0 -10
@@ -1,957 +0,0 @@
1
- import { QrCode } from './QrCode'
2
- import { Tooltip } from './tooltips'
3
- import chat from './svgs/chat.svg?url'
4
- import scan from './svgs/scan.svg?url'
5
- import link from './svgs/link.svg?url'
6
- import qrCode from './svgs/qrcode.svg?url'
7
- import iconCopy from './svgs/icon-copy.svg?url'
8
- import logo from './svgs/logo.svg?url'
9
-
10
- const DEFAULT_REMOTE_URL = 'https://chat.opentiny.design'
11
- const DEFAULT_QR_CODE_URL = 'https://ai.opentiny.design/next-remoter'
12
- const DEFAULT_LOGO_URL = logo
13
-
14
- /** 菜单项配置接口 */
15
- export interface MenuItemConfig {
16
- /** 菜单项标识 */
17
- action: 'qr-code' | 'ai-chat' | 'remote-control' | 'remote-url'
18
- /** 是否显示该菜单项 */
19
- show?: boolean
20
- /** 菜单项文本 */
21
- text?: string
22
- /** 菜单文字颜色 */
23
- active?: boolean
24
- /** 识别码 */
25
- know?: boolean
26
- /** 菜单项描述 */
27
- desc?: string
28
- /** 菜单项提示 */
29
- tip?: string
30
- /** 菜单项图标SVG */
31
- icon?: string
32
- /** 是否显示复制图标 */
33
- showCopyIcon?: boolean
34
- }
35
-
36
- /** 配置选项接口 */
37
- export interface FloatingBlockOptions {
38
- /** 弹出 AI 对话框的回调函数 */
39
- onShowAIChat?: () => void
40
-
41
- /** 遥控端页面地址,默认为: https://ai.opentiny.design/next-remoter */
42
- qrCodeUrl?: string
43
- /** 被遥控页面的 sessionId;无 sessionId 时仅显示「打开对话框」菜单,不显示二维码、识别码、遥控器链接 */
44
- sessionId?: string
45
- /** 菜单项配置 */
46
- menuItems?: MenuItemConfig[]
47
- /** 遥控端页面地址,默认为: https://chat.opentiny.design */
48
- remoteUrl?: string
49
- /** 悬浮Logo的url地址,默认为: https://ai.opentiny.design/next-remoter/svgs/logo-next-no-bg-left.svg */
50
- logoUrl?: string
51
- }
52
-
53
- // 动作类型
54
- export type ActionType = 'qr-code' | 'ai-chat' | 'remote-control' | 'remote-url'
55
-
56
- /** 有 sessionId 时的完整菜单;无 sessionId 时仅返回 ai-chat(不依赖会话的菜单项) */
57
- const getDefaultMenuItems = (options: FloatingBlockOptions): MenuItemConfig[] => {
58
- const hasSession = !!options.sessionId
59
- const baseItems: MenuItemConfig[] = [
60
- {
61
- action: 'qr-code',
62
- show: hasSession,
63
- text: '扫码登录',
64
- desc: '使用手机遥控页面',
65
- icon: qrCode
66
- },
67
- {
68
- action: 'ai-chat',
69
- show: true,
70
- text: '打开对话框',
71
- desc: '支持在网页端操作AI',
72
- icon: chat
73
- },
74
- {
75
- action: 'remote-url',
76
- show: hasSession,
77
- text: `遥控器链接`,
78
- desc: `${options.remoteUrl}`,
79
- active: true,
80
- tip: options.remoteUrl,
81
- showCopyIcon: true,
82
- icon: link
83
- },
84
- {
85
- action: 'remote-control',
86
- show: hasSession,
87
- text: `识别码`,
88
- desc: hasSession ? `${options.sessionId!.slice(-6)}` : '',
89
- know: true,
90
- showCopyIcon: true,
91
- icon: scan
92
- }
93
- ]
94
- return baseItems
95
- }
96
-
97
- class FloatingBlock {
98
- private options: FloatingBlockOptions
99
- private isExpanded = false
100
- private floatingBlock!: HTMLDivElement
101
- private dropdownMenu!: HTMLDivElement
102
- private menuItems: MenuItemConfig[]
103
- /** 即将关闭dropdown的定时器 */
104
- private closingTimer = 0
105
-
106
- // 计算 sessionPrefix 属性
107
- private get sessionPrefix(): string {
108
- return this.options.qrCodeUrl?.includes('?') ? '&sessionId=' : '?sessionId='
109
- }
110
-
111
- constructor(options: FloatingBlockOptions) {
112
- this.options = {
113
- ...options,
114
- qrCodeUrl: options.qrCodeUrl || DEFAULT_QR_CODE_URL,
115
- remoteUrl: options.remoteUrl || DEFAULT_REMOTE_URL,
116
- logoUrl: options.logoUrl || DEFAULT_LOGO_URL
117
- }
118
-
119
- // 合并默认菜单项配置和用户配置。 用户不传入任何menu时, 则不自动合并默认菜单了,即不渲染任何菜单。
120
- if (options.menuItems && options.menuItems.length === 0) {
121
- this.menuItems = []
122
- } else {
123
- this.menuItems = this.mergeMenuItems(options.menuItems)
124
- }
125
-
126
- this.init()
127
- }
128
-
129
- private getImageUrl = (asset: string | undefined): HTMLImageElement | undefined => {
130
- if (!asset) return
131
- const img = new Image()
132
- img.src = asset
133
- return img
134
- }
135
-
136
- private renderItem = (): void => {
137
- this.menuItems
138
- .filter((item) => item.show !== false) // 过滤掉show为false的菜单项
139
- .map((item) => {
140
- const wrapper = document.getElementById(`tiny-remoter-icon-item-${item.action}`) as HTMLDivElement
141
- if (!wrapper) return
142
- wrapper.innerHTML = ''
143
- const img = this.getImageUrl(item.icon)
144
- if (img) wrapper.appendChild(img)
145
- })
146
- }
147
-
148
- /**
149
- * 合并菜单项配置。
150
- * - 用户明确传入 menuItems:直接使用用户配置,不受 sessionId 限制;未传 icon 时自动补充默认图标
151
- * - 有 sessionId 且未传 menuItems:使用默认菜单
152
- * - 无 sessionId 且未传 menuItems:不渲染任何下拉菜单,仅保留点击浮标打开对话框的能力
153
- */
154
- private mergeMenuItems(userMenuItems?: MenuItemConfig[]): MenuItemConfig[] {
155
- // 各 action 对应的默认图标映射
156
- const defaultIcons: Partial<Record<ActionType, string>> = {
157
- 'qr-code': qrCode,
158
- 'ai-chat': chat,
159
- 'remote-url': link,
160
- 'remote-control': scan
161
- }
162
-
163
- // 用户明确传入了 menuItems,直接使用,并补充缺失的图标
164
- if (userMenuItems) {
165
- return userMenuItems.map((item) => ({
166
- ...item,
167
- icon: item.icon ?? defaultIcons[item.action]
168
- }))
169
- }
170
-
171
- // 无 sessionId 且无用户菜单:完全关闭下拉菜单,只保留点击浮标触发 onShowAIChat
172
- if (!this.options.sessionId) {
173
- return []
174
- }
175
-
176
- return getDefaultMenuItems(this.options)
177
- }
178
-
179
- private init(): void {
180
- this.createFloatingBlock()
181
- this.createDropdownMenu()
182
- this.bindEvents()
183
- this.addStyles()
184
- }
185
-
186
- // 创建主浮动块
187
- private createFloatingBlock(): void {
188
- this.floatingBlock = document.createElement('div')
189
- this.floatingBlock.className = 'tiny-remoter-floating-block'
190
- this.floatingBlock.innerHTML = `
191
- <div class="tiny-remoter-floating-block__icon">
192
- <img style="display: block; width: 56px;" src="${this.options.logoUrl}" alt="icon" />
193
- </div>
194
- `
195
-
196
- document.body.appendChild(this.floatingBlock)
197
- }
198
-
199
- private readyTips = (el: string): void => {
200
- const element = document.getElementById(el)
201
- if (!element) {
202
- return
203
- }
204
- new Tooltip(element, {
205
- content: '复制',
206
- placement: 'top',
207
- trigger: 'hover'
208
- })
209
- }
210
-
211
- // 创建下拉菜单
212
- private createDropdownMenu(): void {
213
- this.dropdownMenu = document.createElement('div')
214
- this.dropdownMenu.className = 'tiny-remoter-floating-dropdown'
215
-
216
- // 根据配置动态生成菜单项
217
- const menuItemsHTML = this.menuItems
218
- .filter((item) => item.show !== false) // 过滤掉show为false的菜单项
219
- .map(
220
- (item) => `
221
- <div class="tiny-remoter-dropdown-item" data-action="${item.action}">
222
- <div id="tiny-remoter-icon-item-${item.action}" class="tiny-remoter-dropdown-item__icon">
223
- </div>
224
- <div class="tiny-remoter-dropdown-item__content">
225
- <div title="${item.tip}">${item.text}</div>
226
- <div class="tiny-remoter-dropdown-item__desc-wrapper">
227
- <div class="tiny-remoter-dropdown-item__desc ${item.active ? 'tiny-remoter-dropdown-item__desc--active' : ''} ${item.know ? 'tiny-remoter-dropdown-item__desc--know' : ''}">${item.desc ?? ''}</div>
228
- <div>
229
- ${
230
- item.showCopyIcon
231
- ? `
232
- <div class="tiny-remoter-copy-icon" id="${item.action}" data-action="${item.action}">
233
- <img src="${iconCopy}"/>
234
- </div>
235
- `
236
- : ''
237
- }
238
- </div>
239
- </div>
240
- </div>
241
- </div>
242
- `
243
- )
244
- .join('')
245
-
246
- this.dropdownMenu.innerHTML = menuItemsHTML
247
- document.body.appendChild(this.dropdownMenu)
248
- this.renderItem()
249
- this.readyTips(`remote-control`)
250
- this.readyTips(`remote-url`)
251
- }
252
-
253
- private bindEvents(): void {
254
- // 绑定浮动块点击事件
255
- this.floatingBlock.addEventListener('click', () => {
256
- this.showAIChat()
257
- })
258
-
259
- // 浮动块悬浮处理
260
- this.floatingBlock.addEventListener('mouseenter', () => {
261
- this.openDropdown()
262
-
263
- if (this.closingTimer) {
264
- window.clearTimeout(this.closingTimer)
265
- this.closingTimer = 0
266
- }
267
- })
268
- this.floatingBlock.addEventListener('mouseleave', () => {
269
- this.shouldCloseDropdown()
270
- })
271
- // 悬浮菜单进入,则阻止关闭
272
- this.dropdownMenu.addEventListener('mouseenter', (e: Event) => {
273
- if (this.closingTimer) {
274
- window.clearTimeout(this.closingTimer)
275
- this.closingTimer = 0
276
- }
277
- })
278
- this.dropdownMenu.addEventListener('mouseleave', (e: Event) => {
279
- this.shouldCloseDropdown()
280
- })
281
-
282
- // 绑定菜单项点击事件
283
- this.dropdownMenu.addEventListener('click', (e: Event) => {
284
- const target = e.target as HTMLElement
285
-
286
- // 检查是否点击了复制图标
287
- const copyIcon = target.closest('.tiny-remoter-copy-icon') as HTMLElement
288
- if (copyIcon) {
289
- e.stopPropagation() // 阻止事件冒泡,避免触发菜单项点击
290
- const action = copyIcon.dataset.action as ActionType
291
- if (action) {
292
- this.handleAction(action)
293
- }
294
- return
295
- }
296
-
297
- // 处理普通菜单项点击
298
- const actionItem = target.closest('.tiny-remoter-dropdown-item') as HTMLElement
299
- const action = actionItem?.dataset.action as ActionType
300
- if (action) {
301
- this.handleAction(action)
302
- }
303
- })
304
-
305
- // 点击外部关闭菜单
306
- document.addEventListener('click', (e: Event) => {
307
- const target = e.target as HTMLElement
308
- if (!this.floatingBlock.contains(target) && !this.dropdownMenu.contains(target)) {
309
- this.closeDropdown()
310
- }
311
- })
312
-
313
- // ESC键关闭菜单
314
- document.addEventListener('keydown', (e: KeyboardEvent) => {
315
- if (e.key === 'Escape') {
316
- this.closeDropdown()
317
- }
318
- })
319
- }
320
-
321
- private openDropdown(): void {
322
- // 没有menuItems,则返回
323
- if (!this.menuItems || (this.menuItems && this.menuItems.length === 0)) {
324
- return
325
- }
326
-
327
- this.isExpanded = true
328
- this.floatingBlock.classList.add('expanded')
329
- this.dropdownMenu.classList.add('show')
330
- }
331
-
332
- private shouldCloseDropdown() {
333
- this.closingTimer = window.setTimeout(() => {
334
- this.closeDropdown()
335
- }, 300)
336
- }
337
-
338
- /** 真正的立即关闭 */
339
- private closeDropdown(): void {
340
- this.isExpanded = false
341
- this.floatingBlock.classList.remove('expanded')
342
- this.dropdownMenu.classList.remove('show')
343
- }
344
-
345
- private handleAction(action: ActionType): void {
346
- switch (action) {
347
- case 'qr-code':
348
- this.showQRCode()
349
- break
350
- case 'ai-chat':
351
- this.showAIChat()
352
- break
353
- case 'remote-control':
354
- this.copyRemoteControl()
355
- break
356
- case 'remote-url':
357
- this.copyRemoteURL()
358
- break
359
- }
360
- this.closeDropdown()
361
- }
362
-
363
- private copyRemoteControl(): void {
364
- // 优先使用用户菜单项中的 desc/text(支持自定义识别码),回退到 sessionId 末 6 位
365
- const menuItem = this.menuItems.find((item) => item.action === 'remote-control')
366
- const codeToCopy =
367
- menuItem?.desc || menuItem?.text || (this.options.sessionId ? this.options.sessionId.slice(-6) : '')
368
- if (codeToCopy) {
369
- this.copyToClipboard(codeToCopy)
370
- }
371
- }
372
-
373
- private copyRemoteURL(): void {
374
- const menuItem = this.menuItems.find((item) => item.action === 'remote-url')
375
-
376
- // 构造带 sessionId 的完整遥控链接(默认行为)
377
- const sessionUrl = this.options.sessionId
378
- ? this.options.remoteUrl + this.sessionPrefix + this.options.sessionId
379
- : ''
380
-
381
- // 仅当 desc 是用户真正自定义的值(不同于裸 remoteUrl)时才优先使用,
382
- // 否则回退到带 sessionId 的完整链接,避免默认菜单场景复制裸域名
383
- const customDesc = menuItem?.desc && menuItem.desc !== this.options.remoteUrl ? menuItem.desc : undefined
384
- const urlToCopy = customDesc || sessionUrl || menuItem?.text || ''
385
-
386
- if (urlToCopy) {
387
- this.copyToClipboard(urlToCopy)
388
- }
389
- }
390
-
391
- // 实现复制到剪贴板功能
392
- private async copyToClipboard(text: string): Promise<void> {
393
- try {
394
- // 优先使用现代浏览器的 Clipboard API
395
- if (navigator.clipboard && window.isSecureContext) {
396
- await navigator.clipboard.writeText(text)
397
- this.showCopyFeedback(true)
398
- } else {
399
- // 降级方案:使用传统的 document.execCommand
400
- const textArea = document.createElement('textarea')
401
- textArea.value = text
402
- textArea.style.position = 'fixed'
403
- textArea.style.left = '-999999px'
404
- textArea.style.top = '-999999px'
405
- document.body.appendChild(textArea)
406
- textArea.focus()
407
- textArea.select()
408
-
409
- const successful = document.execCommand('copy')
410
- document.body.removeChild(textArea)
411
-
412
- if (successful) {
413
- this.showCopyFeedback(true)
414
- } else {
415
- this.showCopyFeedback(false)
416
- }
417
- }
418
- } catch (error) {
419
- console.error('复制失败:', error)
420
- this.showCopyFeedback(false)
421
- }
422
- }
423
-
424
- // 显示复制反馈提示
425
- private showCopyFeedback(success: boolean): void {
426
- const message = success ? '复制成功!' : '复制失败,请手动复制'
427
- const feedback = document.createElement('div')
428
- feedback.className = `tiny-remoter-copy-feedback ${success ? 'success' : 'error'}`
429
- feedback.textContent = message
430
-
431
- document.body.appendChild(feedback)
432
-
433
- // 添加显示动画
434
- setTimeout(() => feedback.classList.add('show'), 10)
435
-
436
- // 3秒后自动移除
437
- setTimeout(() => {
438
- feedback.classList.remove('show')
439
- setTimeout(() => {
440
- if (feedback.parentNode) {
441
- feedback.parentNode.removeChild(feedback)
442
- }
443
- }, 300)
444
- }, 1500)
445
- }
446
-
447
- // 创建二维码弹窗(无 sessionId 时不展示)
448
- private async showQRCode(): Promise<void> {
449
- if (!this.options.sessionId) return
450
- const qrCode = new QrCode((this.options.qrCodeUrl || '') + this.sessionPrefix + this.options.sessionId, {})
451
- const base64 = await qrCode.toDataURL()
452
- const modal = this.createModal(
453
- '扫码前往智能遥控器',
454
- `
455
- <div style="text-align: center; padding: 32px;">
456
- <!-- 二维码容器 - 添加渐变背景和阴影效果 -->
457
- <div style="
458
- width: 240px;
459
- height: 240px;
460
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
461
- margin: 0 auto 24px;
462
- display: flex;
463
- align-items: center;
464
- justify-content: center;
465
- border-radius: 20px;
466
- box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);
467
- position: relative;
468
- overflow: hidden;
469
- ">
470
- <!-- 装饰性背景元素 -->
471
- <div style="
472
- position: absolute;
473
- top: -50%;
474
- left: -50%;
475
- width: 200%;
476
- height: 200%;
477
- background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
478
- animation: rotate 20s linear infinite;
479
- "></div>
480
-
481
- <!-- 二维码图片容器 -->
482
- <div style="
483
- width: 200px;
484
- height: 200px;
485
- background: white;
486
- border-radius: 16px;
487
- padding: 16px;
488
- box-shadow: 0 8px 32px rgba(0,0,0,0.1);
489
- position: relative;
490
- z-index: 1;
491
- ">
492
- <img src="${base64}" alt="二维码" style="
493
- width: 100%;
494
- height: 100%;
495
- object-fit: contain;
496
- border-radius: 8px;
497
- ">
498
- </div>
499
- </div>
500
-
501
- <!-- 标题文字 -->
502
- <h3 style="
503
- color: #333;
504
- margin: 0 0 12px 0;
505
- font-size: 20px;
506
- font-weight: 600;
507
- letter-spacing: 0.5px;
508
- ">扫描二维码</h3>
509
-
510
- <!-- 描述文字 -->
511
- <p style="
512
- color: #666;
513
- margin: 0 auto;
514
- margin-bottom: 20px;
515
- font-size: 14px;
516
- line-height: 1.6;
517
- max-width: 280px;
518
- ">请使用手机微信或者浏览器扫描二维码跳转到智能遥控器</p>
519
-
520
- <!-- 提示图标和文字 -->
521
- <div style="
522
- display: flex;
523
- align-items: center;
524
- justify-content: center;
525
- gap: 8px;
526
- color: #999;
527
- font-size: 12px;
528
- ">
529
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7;">
530
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
531
- </svg>
532
- <span>支持微信、浏览器等多种方式</span>
533
- </div>
534
- </div>
535
- `
536
- )
537
- this.showModal(modal)
538
- }
539
-
540
- // 创建AI对话弹窗--- 暂时调 “用户函数”
541
- private showAIChat(): void {
542
- this.options.onShowAIChat?.()
543
- }
544
-
545
- private createModal(title: string, content: string): HTMLDivElement {
546
- const modal = document.createElement('div')
547
- modal.className = 'tiny-remoter-floating-modal'
548
- modal.innerHTML = `
549
- <div class="tiny-remoter-modal-overlay"></div>
550
- <div class="tiny-remoter-modal-content">
551
- <div class="tiny-remoter-modal-header">
552
- <h3>${title}</h3>
553
- <button class="tiny-remoter-modal-close">&times;</button>
554
- </div>
555
- <div class="tiny-remoter-modal-body">
556
- ${content}
557
- </div>
558
- </div>
559
- `
560
-
561
- // 绑定关闭事件
562
- const closeBtn = modal.querySelector('.tiny-remoter-modal-close') as HTMLButtonElement
563
- const overlay = modal.querySelector('.tiny-remoter-modal-overlay') as HTMLDivElement
564
-
565
- closeBtn.addEventListener('click', () => this.hideModal(modal))
566
- overlay.addEventListener('click', () => this.hideModal(modal))
567
-
568
- return modal
569
- }
570
-
571
- private showModal(modal: HTMLDivElement): void {
572
- document.body.appendChild(modal)
573
- // 添加显示动画
574
- setTimeout(() => modal.classList.add('show'), 10)
575
- }
576
-
577
- private hideModal(modal: HTMLDivElement): void {
578
- modal.classList.remove('show')
579
- setTimeout(() => {
580
- if (modal.parentNode) {
581
- modal.parentNode.removeChild(modal)
582
- }
583
- }, 100)
584
- }
585
-
586
- // 创建样式表
587
- private addStyles(): void {
588
- const style = document.createElement('style')
589
- style.textContent = `
590
- /* 浮动块样式 */
591
- .tiny-remoter-floating-block {
592
- position: fixed;
593
- bottom: 30px;
594
- right: 30px;
595
- width: 60px;
596
- height: 60px;
597
- cursor: pointer;
598
- display: flex;
599
- align-items: center;
600
- justify-content: center;
601
- color: white;
602
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
603
- z-index: 99;
604
- overflow: hidden;
605
- border-radius: 50%;
606
- }
607
-
608
- .tiny-remoter-floating-block__icon {
609
- transform: scale(0.8);
610
- transition: transform 0.3s ease;
611
- }
612
-
613
- .tiny-remoter-floating-block__icon:hover {
614
- transform: scale(1.1);
615
- }
616
-
617
- .tiny-remoter-floating-block.expanded .tiny-remoter-floating-block__icon {
618
- transform: scale(1.1);
619
- }
620
-
621
- /* 下拉菜单样式 */
622
- .tiny-remoter-floating-dropdown {
623
- position: fixed;
624
- bottom: 100px;
625
- right: 30px;
626
- background: white;
627
- border-radius: 18px;
628
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
629
- padding: 24px 24px 0px 24px;
630
- opacity: 0;
631
- visibility: hidden;
632
- transform: translateY(20px) scale(0.95);
633
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
634
- z-index: 999;
635
- min-width: 200px;
636
- width: 223px;
637
- height: auto;
638
- }
639
-
640
- .tiny-remoter-floating-dropdown.show {
641
- opacity: 1;
642
- visibility: visible;
643
- transform: translateY(0) scale(1);
644
- }
645
-
646
- .tiny-remoter-dropdown-item {
647
- display: flex;
648
- align-items: center;
649
- border-radius: 12px;
650
- cursor: pointer;
651
- transition: all 0.2s ease;
652
- color: #333;
653
- margin-bottom: 24px;
654
- height: 40px;
655
- }
656
-
657
- .tiny-remoter-dropdown-item:last-child {
658
- margin-bottom: 32px;
659
- }
660
-
661
- .tiny-remoter-dropdown-item > span {
662
- flex: 1;
663
- overflow: hidden;
664
- max-width: 120px;
665
- text-overflow: ellipsis;
666
- white-space: nowrap;
667
- }
668
-
669
- .tiny-remoter-dropdown-item__icon {
670
- display: flex;
671
- align-items: center;
672
- justify-content: center;
673
- width: 40px;
674
- height: 40px;
675
- background: #f8f9fa;
676
- border-radius: 8px;
677
- color: #667eea;
678
- }
679
-
680
- .tiny-remoter-dropdown-item__content {
681
- flex: 1;
682
- display: flex;
683
- flex-direction: column;
684
- gap: 4px;
685
- overflow: hidden;
686
- font-size: 12px;
687
- margin-left: 16px;
688
- }
689
-
690
- .tiny-remoter-dropdown-item__desc-wrapper {
691
- display: flex;
692
- align-items: center;
693
- gap: 4px;
694
- }
695
-
696
- .tiny-remoter-dropdown-item__desc {
697
- color: #808080;
698
- overflow: hidden;
699
- white-space: nowrap;
700
- text-overflow: ellipsis;
701
- }
702
-
703
- .tiny-remoter-dropdown-item__desc--active {
704
- color: #1476ff;
705
- }
706
-
707
- .tiny-remoter-dropdown-item__desc--know {
708
- color: #191919;
709
- }
710
-
711
- /* 复制图标样式 */
712
- .tiny-remoter-copy-icon {
713
- display: flex;
714
- align-items: center;
715
- justify-content: center;
716
- width: 24px;
717
- height: 24px;
718
- background: #f0f0f0;
719
- border-radius: 6px;
720
- color: #666;
721
- cursor: pointer;
722
- transition: all 0.2s ease;
723
- opacity: 0.7;
724
- margin-left: auto;
725
- }
726
-
727
- .tiny-remoter-copy-icon:hover {
728
- background: #e0e0e0;
729
- color: #333;
730
- opacity: 1;
731
- transform: scale(1.1);
732
- }
733
-
734
- .tiny-remoter-copy-icon:active {
735
- transform: scale(0.95);
736
- }
737
-
738
- /* 弹窗样式 */
739
- .tiny-remoter-floating-modal {
740
- position: fixed;
741
- top: 0;
742
- left: 0;
743
- width: 100%;
744
- height: 100%;
745
- z-index: 2000;
746
- display: flex;
747
- align-items: center;
748
- justify-content: center;
749
- }
750
-
751
- .tiny-remoter-modal-overlay {
752
- position: absolute;
753
- top: 0;
754
- left: 0;
755
- width: 100%;
756
- height: 100%;
757
- background: rgba(0, 0, 0, 0.5);
758
- backdrop-filter: blur(4px);
759
- opacity: 0;
760
- transition: opacity 0.3s ease;
761
- }
762
-
763
- .tiny-remoter-modal-content {
764
- background: white;
765
- border-radius: 16px;
766
- box-shadow: 0 25px 80px rgba(0, 0, 0, 0.2);
767
- max-width: 500px;
768
- width: 90%;
769
- max-height: 80vh;
770
- overflow: hidden;
771
- transform: scale(0.9) translateY(20px);
772
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
773
- }
774
-
775
- .tiny-remoter-floating-modal.show .tiny-remoter-modal-overlay {
776
- opacity: 1;
777
- }
778
-
779
- .tiny-remoter-floating-modal.show .tiny-remoter-modal-content {
780
- transform: scale(1) translateY(0);
781
- }
782
-
783
- .tiny-remoter-modal-header {
784
- display: flex;
785
- align-items: center;
786
- justify-content: space-between;
787
- padding: 20px 24px;
788
- border-bottom: 1px solid #f0f0f0;
789
- }
790
-
791
- .tiny-remoter-modal-header h3 {
792
- margin: 0;
793
- font-size: 18px;
794
- font-weight: 600;
795
- color: #333;
796
- }
797
-
798
- .tiny-remoter-modal-close {
799
- background: none;
800
- border: none;
801
- font-size: 24px;
802
- cursor: pointer;
803
- color: #999;
804
- padding: 0;
805
- width: 32px;
806
- height: 32px;
807
- border-radius: 50%;
808
- display: flex;
809
- align-items: center;
810
- justify-content: center;
811
- transition: all 0.2s ease;
812
- }
813
-
814
- .tiny-remoter-modal-close:hover {
815
- background: #f5f5f5;
816
- color: #666;
817
- }
818
-
819
- .tiny-remoter-modal-body {
820
- padding: 24px;
821
- }
822
-
823
- /* 二维码弹窗动画 */
824
- @keyframes rotate {
825
- from {
826
- transform: rotate(0deg);
827
- }
828
- to {
829
- transform: rotate(360deg);
830
- }
831
- }
832
-
833
- /* 响应式设计 */
834
- @media (max-width: 768px) {
835
- .tiny-remoter-floating-block {
836
- bottom: 20px;
837
- right: 20px;
838
- width: 56px;
839
- height: 56px;
840
- }
841
-
842
- .tiny-remoter-floating-dropdown {
843
- bottom: 90px;
844
- right: 20px;
845
- min-width: 180px;
846
- }
847
-
848
- .tiny-remoter-modal-content {
849
- width: 95%;
850
- margin: 20px;
851
- }
852
- }
853
-
854
- /* 复制反馈提示样式 */
855
- .tiny-remoter-copy-feedback {
856
- position: fixed;
857
- top: 50%;
858
- left: 50%;
859
- transform: translate(-50%, -50%);
860
- background: rgba(0, 0, 0, 0.8);
861
- color: white;
862
- padding: 12px 24px;
863
- border-radius: 8px;
864
- font-size: 14px;
865
- font-weight: 500;
866
- z-index: 10000;
867
- opacity: 0;
868
- visibility: hidden;
869
- transition: all 0.3s ease;
870
- backdrop-filter: blur(4px);
871
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
872
- }
873
-
874
- .tiny-remoter-copy-feedback.show {
875
- opacity: 1;
876
- visibility: visible;
877
- transform: translate(-50%, -50%) scale(1);
878
- }
879
-
880
- .tiny-remoter-copy-feedback.success {
881
- background: rgba(34, 197, 94, 0.9);
882
- }
883
-
884
- .tiny-remoter-copy-feedback.error {
885
- background: rgba(239, 68, 68, 0.9);
886
- }
887
-
888
- /* 深色主题支持 */
889
- @media (prefers-color-scheme: dark) {
890
- .tiny-remoter-floating-dropdown {
891
- background: #1a1a1a;
892
- color: white;
893
- }
894
-
895
- .tiny-remoter-dropdown-item {
896
- color: white;
897
- }
898
-
899
-
900
- .tiny-remoter-copy-icon {
901
- background: #2a2a2a;
902
- color: #ccc;
903
- }
904
-
905
- .tiny-remoter-copy-icon:hover {
906
- background: #3a3a3a;
907
- color: white;
908
- }
909
-
910
- .tiny-remoter-modal-content {
911
- background: #1a1a1a;
912
- color: white;
913
- }
914
-
915
- .tiny-remoter-modal-header {
916
- border-bottom-color: #333;
917
- }
918
-
919
- .tiny-remoter-modal-header h3 {
920
- color: white;
921
- }
922
- }
923
- `
924
-
925
- document.head.appendChild(style)
926
- }
927
-
928
- // 销毁组件
929
- public destroy(): void {
930
- if (this.floatingBlock.parentNode) {
931
- this.floatingBlock.parentNode.removeChild(this.floatingBlock)
932
- }
933
- if (this.dropdownMenu.parentNode) {
934
- this.dropdownMenu.parentNode.removeChild(this.dropdownMenu)
935
- }
936
- }
937
-
938
- // 隐藏组件
939
- public hide(): void {
940
- if (this.floatingBlock) {
941
- this.floatingBlock.style.display = 'none'
942
- }
943
- this.closeDropdown()
944
- }
945
-
946
- // 显示组件
947
- public show(): void {
948
- if (this.floatingBlock) {
949
- this.floatingBlock.style.display = 'flex'
950
- }
951
- }
952
- }
953
-
954
- // 导出组件
955
- export const createRemoter = (options = {} as FloatingBlockOptions) => {
956
- return new FloatingBlock(options)
957
- }