widget.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * AI-CS 访客聊天小窗插件
  3. * 可嵌入到任何网站中,提供客服聊天功能
  4. *
  5. * 使用方法:
  6. * <script src="https://your-domain.com/widget.js"></script>
  7. * <script>
  8. * AICSWidget.init({
  9. * apiUrl: 'https://your-api-domain.com',
  10. * position: 'bottom-right' // 可选:'bottom-right' | 'bottom-left'
  11. * });
  12. * </script>
  13. */
  14. (function() {
  15. 'use strict';
  16. // 配置(从全局变量或默认值读取后端端口)
  17. const getDefaultBackendPort = () => {
  18. // 优先使用全局变量(可在页面中通过 script 标签设置)
  19. if (typeof window !== 'undefined' && window.AICS_BACKEND_PORT) {
  20. return window.AICS_BACKEND_PORT;
  21. }
  22. // 默认端口 18080(避免与常用端口冲突)
  23. return '18080';
  24. };
  25. const defaultConfig = {
  26. apiUrl: 'http://localhost:' + getDefaultBackendPort(),
  27. position: 'bottom-right', // 'bottom-right' | 'bottom-left'
  28. theme: 'default'
  29. };
  30. let config = { ...defaultConfig };
  31. let widgetContainer = null;
  32. let isInitialized = false;
  33. /**
  34. * 创建浮动按钮
  35. */
  36. function createFloatingButton() {
  37. const button = document.createElement('button');
  38. button.className = 'ai-cs-widget-button';
  39. button.setAttribute('aria-label', '打开客服聊天');
  40. button.innerHTML = `
  41. <svg class="ai-cs-widget-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  42. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
  43. </svg>
  44. `;
  45. // 样式
  46. const position = config.position === 'bottom-left' ? 'left: 1rem;' : 'right: 1rem;';
  47. button.style.cssText = `
  48. position: fixed;
  49. bottom: 1rem;
  50. ${position}
  51. width: 3.5rem;
  52. height: 3.5rem;
  53. border-radius: 9999px;
  54. background-color: #3b82f6;
  55. color: white;
  56. border: none;
  57. cursor: pointer;
  58. box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  59. z-index: 9999;
  60. display: flex;
  61. align-items: center;
  62. justify-content: center;
  63. transition: all 0.3s;
  64. `;
  65. button.onmouseover = function() {
  66. this.style.transform = 'scale(1.1)';
  67. this.style.boxShadow = '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)';
  68. };
  69. button.onmouseout = function() {
  70. this.style.transform = 'scale(1)';
  71. this.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
  72. };
  73. button.onclick = function() {
  74. toggleWidget();
  75. };
  76. return button;
  77. }
  78. /**
  79. * 创建聊天窗口 iframe
  80. */
  81. function createChatWindow() {
  82. const iframe = document.createElement('iframe');
  83. iframe.id = 'ai-cs-widget-iframe';
  84. iframe.src = config.chatPageUrl || `${config.apiUrl.replace('/api', '')}/chat`;
  85. iframe.style.cssText = `
  86. position: fixed;
  87. bottom: 5rem;
  88. ${config.position === 'bottom-left' ? 'left: 1rem;' : 'right: 1rem;'}
  89. width: 400px;
  90. max-width: calc(100vw - 2rem);
  91. height: 600px;
  92. max-height: calc(100vh - 6rem);
  93. border: none;
  94. border-radius: 0.5rem;
  95. box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  96. z-index: 9998;
  97. display: none;
  98. background: white;
  99. `;
  100. return iframe;
  101. }
  102. /**
  103. * 切换聊天窗口显示/隐藏
  104. */
  105. function toggleWidget() {
  106. const iframe = document.getElementById('ai-cs-widget-iframe');
  107. if (iframe) {
  108. const isVisible = iframe.style.display !== 'none';
  109. iframe.style.display = isVisible ? 'none' : 'block';
  110. // 更新按钮图标
  111. const button = document.querySelector('.ai-cs-widget-button');
  112. if (button) {
  113. const icon = button.querySelector('.ai-cs-widget-icon');
  114. if (icon) {
  115. if (isVisible) {
  116. // 显示聊天图标
  117. icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>';
  118. } else {
  119. // 显示关闭图标
  120. icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>';
  121. }
  122. }
  123. }
  124. }
  125. }
  126. /**
  127. * 初始化插件
  128. */
  129. function init(userConfig) {
  130. if (isInitialized) {
  131. console.warn('AI-CS Widget 已经初始化');
  132. return;
  133. }
  134. // 合并配置
  135. config = { ...defaultConfig, ...userConfig };
  136. // 创建容器
  137. widgetContainer = document.createElement('div');
  138. widgetContainer.id = 'ai-cs-widget-container';
  139. widgetContainer.style.cssText = 'position: fixed; bottom: 0; z-index: 9999;';
  140. // 创建浮动按钮
  141. const button = createFloatingButton();
  142. widgetContainer.appendChild(button);
  143. // 创建聊天窗口
  144. const iframe = createChatWindow();
  145. widgetContainer.appendChild(iframe);
  146. // 添加到页面
  147. document.body.appendChild(widgetContainer);
  148. isInitialized = true;
  149. console.log('AI-CS Widget 初始化成功');
  150. }
  151. /**
  152. * 销毁插件
  153. */
  154. function destroy() {
  155. if (widgetContainer && widgetContainer.parentNode) {
  156. widgetContainer.parentNode.removeChild(widgetContainer);
  157. widgetContainer = null;
  158. isInitialized = false;
  159. console.log('AI-CS Widget 已销毁');
  160. }
  161. }
  162. // 暴露全局 API
  163. window.AICSWidget = {
  164. init: init,
  165. destroy: destroy,
  166. toggle: toggleWidget
  167. };
  168. // 如果 DOM 已加载,自动初始化(使用默认配置)
  169. if (document.readyState === 'loading') {
  170. document.addEventListener('DOMContentLoaded', function() {
  171. // 不自动初始化,需要用户手动调用 AICSWidget.init()
  172. });
  173. }
  174. })();