NotebookGlobalDragDropOverlay.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { useLayoutEffect, useRef, useState, useCallback } from 'react';
  2. import { useLanguage } from '../contexts/LanguageContext';
  3. interface NotebookGlobalDragDropProps {
  4. onFilesSelected: (files: FileList) => void;
  5. isAdmin: boolean;
  6. }
  7. // 为笔记本组件也添加类似的控制
  8. let isNotebookDragDropEnabled = true;
  9. // 添加一个模块级变量来存储强制隐藏的回调函数
  10. let notebookForceHideCallback: (() => void) | null = null;
  11. export const setNotebookDragDropEnabled = (enabled: boolean) => {
  12. isNotebookDragDropEnabled = enabled;
  13. // 当禁用时,立即强制隐藏覆盖层
  14. if (!enabled && notebookForceHideCallback) {
  15. notebookForceHideCallback();
  16. }
  17. };
  18. export const NotebookGlobalDragDropOverlay: React.FC<NotebookGlobalDragDropProps> = ({ onFilesSelected, isAdmin }) => {
  19. const { t } = useLanguage();
  20. const overlayRef = useRef<HTMLDivElement>(null);
  21. const [isVisible, setIsVisible] = useState(false);
  22. const dragCounterRef = useRef(0);
  23. const isDragActiveRef = useRef(false);
  24. const hasFiles = useCallback((dt: DataTransfer | null) => {
  25. if (!dt) return false;
  26. // より厳格なチェック: typesにFilesが含まれることを確認
  27. const hasFileType = dt.types && dt.types.includes('Files');
  28. if (!hasFileType) return false;
  29. // itemsが存在する場合、実際にファイルがあるかチェック
  30. if (dt.items && dt.items.length > 0) {
  31. // 少なくとも1つのファイルアイテムがあることを確認
  32. for (let i = 0; i < dt.items.length; i++) {
  33. if (dt.items[i].kind === 'file') {
  34. return true;
  35. }
  36. }
  37. return false;
  38. }
  39. // itemsがない場合はtypesのみで判断(後方互換性)
  40. return hasFileType;
  41. }, []);
  42. const handleDragEnter = useCallback((e: DragEvent) => {
  43. // 如果拖放功能被禁用,则忽略事件
  44. if (!isNotebookDragDropEnabled) return;
  45. // 只有当数据传输包含文件时才处理
  46. if (!hasFiles(e.dataTransfer)) return;
  47. e.preventDefault();
  48. e.stopPropagation();
  49. dragCounterRef.current++;
  50. if (dragCounterRef.current === 1) {
  51. // 立即显示覆盖层,依赖强制隐藏机制来处理误触发
  52. setIsVisible(true);
  53. if (overlayRef.current) {
  54. overlayRef.current.style.opacity = '1';
  55. overlayRef.current.style.visibility = 'visible';
  56. }
  57. isDragActiveRef.current = true;
  58. }
  59. }, [hasFiles]);
  60. const handleDragOver = useCallback((e: DragEvent) => {
  61. // 如果拖放功能被禁用,则忽略事件
  62. if (!isNotebookDragDropEnabled) return;
  63. // 只有当数据传输包含文件时才处理
  64. if (!hasFiles(e.dataTransfer)) return;
  65. e.preventDefault();
  66. e.stopPropagation();
  67. e.dataTransfer!.dropEffect = 'copy';
  68. }, [hasFiles]);
  69. const handleDragLeave = useCallback((e: DragEvent) => {
  70. // 如果拖放功能被禁用,则忽略事件
  71. if (!isNotebookDragDropEnabled) return;
  72. // 只有当数据传输包含文件时才处理
  73. if (!hasFiles(e.dataTransfer)) return;
  74. e.preventDefault();
  75. e.stopPropagation();
  76. dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
  77. if (dragCounterRef.current === 0 && isDragActiveRef.current) {
  78. // 隐藏全局拖拽上传覆盖层
  79. if (overlayRef.current) {
  80. overlayRef.current.style.opacity = '0';
  81. overlayRef.current.style.visibility = 'hidden';
  82. }
  83. setIsVisible(false); // 通过状态控制隐藏
  84. isDragActiveRef.current = false;
  85. }
  86. }, [hasFiles]);
  87. const handleDrop = useCallback((e: DragEvent) => {
  88. // 如果拖放功能被禁用,则忽略事件
  89. if (!isNotebookDragDropEnabled) return;
  90. // 只有当数据传输包含文件时才处理
  91. if (!hasFiles(e.dataTransfer)) return;
  92. e.preventDefault();
  93. e.stopPropagation();
  94. dragCounterRef.current = 0;
  95. // 隐藏全局拖拽上传覆盖层
  96. if (overlayRef.current) {
  97. overlayRef.current.style.opacity = '0';
  98. overlayRef.current.style.visibility = 'hidden';
  99. }
  100. setIsVisible(false);
  101. isDragActiveRef.current = false;
  102. // 处理文件
  103. if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
  104. onFilesSelected(e.dataTransfer.files);
  105. }
  106. }, [hasFiles, onFilesSelected]);
  107. useLayoutEffect(() => {
  108. if (!isAdmin) return;
  109. // 初始化时确保dragCounter和isDragActive为初始值
  110. dragCounterRef.current = 0;
  111. isDragActiveRef.current = false;
  112. // 注册强制隐藏回调
  113. notebookForceHideCallback = () => {
  114. dragCounterRef.current = 0;
  115. isDragActiveRef.current = false;
  116. setIsVisible(false);
  117. if (overlayRef.current) {
  118. overlayRef.current.style.opacity = '0';
  119. overlayRef.current.style.visibility = 'hidden';
  120. }
  121. };
  122. // 添加全局事件监听器
  123. document.addEventListener('dragenter', handleDragEnter);
  124. document.addEventListener('dragover', handleDragOver);
  125. document.addEventListener('dragleave', handleDragLeave);
  126. document.addEventListener('drop', handleDrop);
  127. // 清理函数
  128. return () => {
  129. document.removeEventListener('dragenter', handleDragEnter);
  130. document.removeEventListener('dragover', handleDragOver);
  131. document.removeEventListener('dragleave', handleDragLeave);
  132. document.removeEventListener('drop', handleDrop);
  133. // 清除回调引用
  134. notebookForceHideCallback = null;
  135. // 确保在组件卸载时重置状态和清除任何可能的显示
  136. dragCounterRef.current = 0;
  137. isDragActiveRef.current = false;
  138. if (overlayRef.current) {
  139. overlayRef.current.style.opacity = '0';
  140. overlayRef.current.style.visibility = 'hidden';
  141. }
  142. setIsVisible(false);
  143. };
  144. }, [isAdmin, handleDragEnter, handleDragOver, handleDragLeave, handleDrop]);
  145. // 添加运行时检查,确保只有在适当情况下才渲染
  146. if (!isAdmin || typeof window === 'undefined') {
  147. return null;
  148. }
  149. // 只有当 isVisible 为 true 时才渲染组件内容
  150. if (!isVisible) {
  151. return null;
  152. }
  153. return (
  154. <div
  155. ref={overlayRef}
  156. id="notebook-global-drag-overlay"
  157. className="fixed inset-0 bg-black bg-opacity-50 items-center justify-center z-50 transition-opacity duration-300 pointer-events-none"
  158. style={{ opacity: 1, visibility: 'visible', display: 'flex' }}
  159. >
  160. <div className="w-3/4 max-w-2xl pointer-events-auto">
  161. <div className="border-2 border-dashed border-blue-500 bg-blue-50 rounded-xl p-8 text-center cursor-pointer">
  162. <div className="flex flex-col items-center justify-center gap-6">
  163. <div className="p-4 bg-blue-100 rounded-full">
  164. <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-10 h-10 text-blue-600">
  165. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  166. <polyline points="17 8 12 3 7 8"></polyline>
  167. <line x1="12" x2="12" y1="3" y2="15"></line>
  168. </svg>
  169. </div>
  170. <div className="space-y-2">
  171. <h3 className="text-lg font-semibold text-slate-700">
  172. {t('dragDropUploadTitle')}
  173. </h3>
  174. <p className="text-sm text-slate-500">
  175. {t('dragDropUploadDesc')}
  176. </p>
  177. </div>
  178. <div className="flex items-center gap-6 mt-2">
  179. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  180. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
  181. <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
  182. <polyline points="14 2 14 8 20 8"></polyline>
  183. </svg>
  184. <span>{t('supportedFormats')}</span>
  185. </div>
  186. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  187. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
  188. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  189. <circle cx="8.5" cy="8.5" r="1.5"></circle>
  190. <path d="M21 15l-5-5L5 21"></path>
  191. </svg>
  192. <span>PDF, DOC, XLS, PPT, TXT, Images...</span>
  193. </div>
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. </div>
  199. );
  200. };