开发onlyoffice插件,功能是选择文本后立即通知父页面

code.js

javascript 复制代码
console.log("work")
window.Asc.plugin.init = function init() { }
window.Asc.plugin.button = function button(id) {
  this.executeCommand("close", "")
}
let isShowMenu = false
if (isShowMenu) {
  window.Asc.plugin.event_onContextMenuShow = function event_onContextMenuShow(options) {
    switch (options.type) {
      case "Selection":
        this.executeMethod("AddContextMenuItem", [{
          guid: this.guid,
          items: [
            {
              id: "onClickAddToChat",
              text: '添加到对话',
            },
            {
              id: "onClickTranslation",
              text: '翻译',
            },
          ],
        }])
        break
      default:
        break
    }
  }

  window.Asc.plugin.event_onContextMenuClick = (id) => {
    const pluginObj = window.Asc.plugin
    let itemId = id
    let itemData = undefined
    const itemPos = itemId.indexOf("_oo_sep_")
    if (itemPos !== -1) {
      itemData = itemId.slice(itemPos + 8)
      itemId = itemId.slice(0, itemPos)
    }

    if (pluginObj.contextMenuEvents && pluginObj.contextMenuEvents[itemId]) {
      pluginObj.contextMenuEvents[itemId].call(pluginObj, itemData)
    }
  }

  window.Asc.plugin.attachContextMenuClickEvent("onClickAddToChat", () => {
    window.Asc.plugin.executeMethod("GetSelectedText", [], function (text) {
      const message = {
        type: 'ADD_TO_CHAT',
        text: text,
        timestamp: Date.now()
      };

      try {
        if (window.top && window.top !== window) {
          window.top.postMessage(message, '*');
        }
      } catch (e) {
        console.log('无法访问 window.top:', e);
      }
    });
  })

  window.Asc.plugin.attachContextMenuClickEvent("onClickTranslation", () => {
    window.Asc.plugin.executeMethod("GetSelectedText", [], function (text) {
      const message = {
        type: 'TRANSLATE_TEXT',
        text: text,
        timestamp: Date.now()
      };

      try {
        if (window.top && window.top !== window) {
          window.top.postMessage(message, '*');
        }
      } catch (e) {
        console.log('无法访问 window.top:', e);
      }
    });
  })
}

let isSelectMonitor = true;
if (isSelectMonitor) {
  window.Asc.plugin.event_onContextMenuShow = function (options) {
    sendHideSelectionToParent();
  }

  window.Asc.plugin.event_onFocusOut = function () {
    checkAndSendSelection();
  }

  window.Asc.plugin.event_onBlur = function () {
    checkAndSendSelection();
  }

  function checkAndSendSelection() {
    window.Asc.plugin.executeMethod("GetSelectedText", [], function (text) {
      if (text && text.trim()) {
        sendSelectionToParent(text);
      }
    });
  }

  function sendSelectionToParent(text, position) {
    const message = {
      type: 'TEXT_SELECTED',
      text: text,
      timestamp: Date.now()
    };

    try {
      if (window.top && window.top !== window) {
        window.top.postMessage(message, '*');
      }
    } catch (e) {
      console.log('无法访问 window.top:', e);
    }

    try {
      if (window.parent && window.parent !== window) {
        window.parent.postMessage(message, '*');
        console.log('已发送到 window.parent');
      }
    } catch (e) {
      console.log('无法访问 window.parent:', e);
    }

    try {
      if (window.parent.parent && window.parent.parent !== window) {
        window.parent.parent.postMessage(message, '*');
        console.log('已发送到 window.parent.parent');
      }
    } catch (e) {
      console.log('无法访问 window.parent.parent:', e);
    }    
  }

  function sendHideSelectionToParent(text, position) {
    const message = {
      type: 'HIDE_TEXT_SELECTED',
      timestamp: Date.now()
    };

    try {
      if (window.top && window.top !== window) {
        window.top.postMessage(message, '*');
      }
    } catch (e) {
      console.log('无法访问 window.top:', e);
    }

    try {
      if (window.parent && window.parent !== window) {
        window.parent.postMessage(message, '*');
        console.log('已发送到 window.parent');
      }
    } catch (e) {
      console.log('无法访问 window.parent:', e);
    }

    try {
      if (window.parent.parent && window.parent.parent !== window) {
        window.parent.parent.postMessage(message, '*');
        console.log('已发送到 window.parent.parent');
      }
    } catch (e) {
      console.log('无法访问 window.parent.parent:', e);
    }    

  }

  let lastSelectedText = '';
  let selectionCheckInterval = null;

  function startSelectionMonitoring() {
    if (selectionCheckInterval) {
      clearInterval(selectionCheckInterval);
    }

    selectionCheckInterval = setInterval(() => {
      window.Asc.plugin.executeMethod("GetSelectedText", [], function (text) {
        if (text && text.trim() && text !== lastSelectedText) {
          lastSelectedText = text;
          sendSelectionToParent(text);
        } else if (!text || !text.trim()) {
          sendHideSelectionToParent()
          lastSelectedText = '';
        }
      });
    }, 500);
  }
  startSelectionMonitoring();
}

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>example context menu and events</title>
        <script type="text/javascript" src="https://onlyoffice.github.io/sdkjs-plugins/v1/plugins.js"></script>    
        <script src="scripts/code.js"></script>
    </head>
    <body>
    </body>
</html>

config.json

javascript 复制代码
{
  "name": "工具",
  "guid": "asc.008",

  "variations": [
    {
      "description": "example context menu and events",
      "url": "index.html",

      "icons": ["resources/light/icon.png", "resources/light/icon@2x.png"],
      "icons2": [
        {
          "style": "light",
                    
          "100%": {
            "normal": "resources/light/icon.png"
          },
          "125%": {
            "normal": "resources/light/icon@1.25x.png"
          },
          "150%": {
            "normal": "resources/light/icon@1.5x.png"
          },
          "175%": {
            "normal": "resources/light/icon@1.75x.png"
          },
          "200%": {
            "normal": "resources/light/icon@2x.png"
          }
        },
        {
          "style": "dark",
                    
          "100%": {
            "normal": "resources/dark/icon.png"
          },
          "125%": {
            "normal": "resources/dark/icon@1.25x.png"
          },
          "150%": {
            "normal": "resources/dark/icon@1.5x.png"
          },
          "175%": {
            "normal": "resources/dark/icon@1.75x.png"
          },
          "200%": {
            "normal": "resources/dark/icon@2x.png"
          }
        }
      ],
      "isViewer": false,
      "EditorsSupport": ["word", "cell", "slide"],

      "isVisual": false,
      "isModal": false,
      "isInsideMode": false,
      "isSystem": false,

      "initDataType": "none",
      "initData": "",

      "buttons": [],

      "events": [
        "onContextMenuShow",
        "onContextMenuClick",
        "onTargetPositionChanged",
        "onFocusOut",
        "onBlur"
      ]
    }
  ]
}

父页面:

javascript 复制代码
'use client'

import { useState, useRef, useEffect } from 'react'
import Header from '@/components/header'
import {
  ArrowLeft,
  Upload,
  FileText,
  File,
  CheckCircle2,
  AlertCircle,
  Loader2,
  X,
  FolderOpen,
  Sparkles,
  Copy,
  Search,
  BookOpen,
  MessageSquare,
  Wand2,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import Script from 'next/script'
import { cn } from '@/lib/utils'

// 声明全局 DocsAPI 类型
declare global {
  interface Window {
    DocsAPI?: {
      DocEditor: new (id: string, config: any) => any
    }
  }
}

// OnlyOffice Editor 实例类型
interface DocEditorInstance {
  destroyEditor: () => void
  downloadAs: (format: string) => void
  requestClose: () => void
}

// 支持的文件格式
const SUPPORTED_FORMATS = {
  documents: [
    '.doc',
    '.docx',
    '.docm',
    '.dot',
    '.dotx',
    '.dotm',
    '.odt',
    '.fodt',
    '.ott',
    '.rtf',
    '.txt',
    '.html',
    '.htm',
    '.mht',
    '.pdf',
    '.djvu',
    '.fb2',
    '.epub',
    '.xps',
  ],
  spreadsheets: [
    '.xls',
    '.xlsx',
    '.xlsm',
    '.xlt',
    '.xltx',
    '.xltm',
    '.ods',
    '.fods',
    '.ots',
    '.csv',
  ],
  presentations: [
    '.pps',
    '.ppsx',
    '.ppsm',
    '.ppt',
    '.pptx',
    '.pptm',
    '.pot',
    '.potx',
    '.potm',
    '.odp',
    '.fodp',
    '.otp',
  ],
}

const ALL_FORMATS = [
  ...SUPPORTED_FORMATS.documents,
  ...SUPPORTED_FORMATS.spreadsheets,
  ...SUPPORTED_FORMATS.presentations,
]

interface UploadedFile {
  file: File
  name: string
  size: number
  type: string
  uploadTime: number
  previewUrl?: string
}

export default function OnlyOfficePage() {
  const router = useRouter()
  const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null)
  const [isUploading, setIsUploading] = useState(false)
  const [isDragOver, setIsDragOver] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [isPreviewReady, setIsPreviewReady] = useState(false)
  const [scriptLoaded, setScriptLoaded] = useState(false)
  const fileInputRef = useRef<HTMLInputElement>(null)
  const editorContainerRef = useRef<HTMLDivElement>(null)
  const docEditorRef = useRef<DocEditorInstance | null>(null)

  // 鼠标位置追踪
  const mousePositionRef = useRef({ x: 0, y: 0 })

  // 上下文菜单状态
  const [contextMenu, setContextMenu] = useState<{
    visible: boolean
    x: number
    y: number
    selectedText: string
  }>({
    visible: false,
    x: 0,
    y: 0,
    selectedText: '',
  })

  // 格式化文件大小
  const formatBytes = (bytes: number): string => {
    if (bytes === 0) return '0 B'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
  }

  // 检查文件格式
  const isFileSupported = (fileName: string): boolean => {
    const ext = '.' + fileName.split('.').pop()?.toLowerCase()
    return ALL_FORMATS.includes(ext)
  }

  // 获取文件类型
  const getFileType = (
    fileName: string,
  ): 'word' | 'cell' | 'slide' | 'unknown' => {
    const ext = '.' + fileName.split('.').pop()?.toLowerCase()
    if (SUPPORTED_FORMATS.documents.includes(ext)) return 'word'
    if (SUPPORTED_FORMATS.spreadsheets.includes(ext)) return 'cell'
    if (SUPPORTED_FORMATS.presentations.includes(ext)) return 'slide'
    return 'unknown'
  }

  // 处理文件选择
  const handleFileSelect = async (file: File) => {
    setError(null)
    setIsPreviewReady(false)

    if (!isFileSupported(file.name)) {
      setError(
        `不支持的文件格式。支持的格式包括:${ALL_FORMATS.slice(0, 10).join(', ')} 等`,
      )
      return
    }

    setIsUploading(true)

    try {
      // 模拟上传过程
      await new Promise((resolve) => setTimeout(resolve, 800))

      const uploadedFileData: UploadedFile = {
        file,
        name: file.name,
        size: file.size,
        type: getFileType(file.name),
        uploadTime: Date.now(),
        previewUrl: URL.createObjectURL(file),
      }

      setUploadedFile(uploadedFileData)

      // 延迟显示预览准备就绪
      setTimeout(() => {
        setIsPreviewReady(true)
        initOnlyOfficeEditor(uploadedFileData)
      }, 500)
    } catch (err: any) {
      console.error('上传失败:', err)
      setError(err.message || '上传失败,请重试')
    } finally {
      setIsUploading(false)
    }
  }

  // 初始化 OnlyOffice 编辑器
  const initOnlyOfficeEditor = (fileData: any, type: any) => {
    if (!editorContainerRef.current || !window.DocsAPI) {
      console.warn('OnlyOffice API 未加载或容器未准备好')
      return
    }
    let url = 'https://static.xutongbao.top/onlyoffice/3.docx'
    if (type === '1') {
      fileData = {
        type: 'word',
        uploadTime: 1768789600204,
        name: '测试.docx',
      }
      url = 'https://static.xutongbao.top/onlyoffice/3.docx'
    } else if (type === '2') {
      fileData = {
        type: 'cell',
        uploadTime: 1768789600204,
        name: '测试.xlsx',
      }
      url = 'https://static.xutongbao.top/onlyoffice/1.xlsx'
    } else if (type === '3') {
      fileData = {
        type: 'slide',
        uploadTime: 1768789600204,
        name: '测试.pptx',
      }
      url = 'https://static.xutongbao.top/onlyoffice/1.pptx'
    }

    // 实际项目中需要配置 OnlyOffice Document Server
    // 示例配置(需要根据实际情况调整):

    const config = {
      documentType: fileData.type,
      document: {
        fileType: fileData.name.split('.').pop(),
        key: `${fileData.uploadTime}-${Math.random().toString(36).substring(7)}`,
        title: fileData.name,
        url,
      },
      editorConfig: {
        mode: 'edit', // 编辑模式
        lang: 'zh-CN',
        user: {
          id: '690313ca3814f11a1fb7cbd7',
          name: '徐同保',
        },
        // 启用文本选中监听插件
        plugins: {
          autostart: ['asc.008'],
        },
      },
      width: '100%',
      height: '100%',
      events: {
        onDocumentReady: () => {
          console.log('OnlyOffice 文档加载完成')
        },
        onAppReady: () => {
          console.log('OnlyOffice 应用已准备就绪')
        },
      },
    }

    const editor = new window.DocsAPI.DocEditor('onlyoffice-editor', config)
    docEditorRef.current = editor
  }

  // 监听 OnlyOffice API 脚本加载
  useEffect(() => {
    if (window.DocsAPI && scriptLoaded && uploadedFile && isPreviewReady) {
      // initOnlyOfficeEditor(uploadedFile)
    }
  }, [scriptLoaded, uploadedFile, isPreviewReady])

  // 监听鼠标移动,追踪鼠标坐标
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      mousePositionRef.current = { x: e.clientX, y: e.clientY }
      console.log('鼠标位置更新:', mousePositionRef.current)
    }

    window.addEventListener('mousemove', handleMouseMove)
    return () => {
      window.removeEventListener('mousemove', handleMouseMove)
    }
  }, [])

  // 监听来自 OnlyOffice 的消息
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      // 监听来自 OnlyOffice iframe 的消息
      if (event.data && typeof event.data === 'object') {
        // 添加到对话消息
        if (event.data.type === 'ADD_TO_CHAT') {
          console.log('✓ 收到添加到对话请求:', event.data.text)
          // 这里可以调用你的对话功能
          alert(`添加到对话:\n${event.data.text}`)
        }

        // 翻译消息
        if (event.data.type === 'TRANSLATE_TEXT') {
          console.log('✓ 收到翻译请求:', event.data.text)
          // 这里可以调用你的翻译功能
          alert(`翻译文本:\n${event.data.text}`)
        }

        // 文本选中消息 - 使用父页面捕获的鼠标坐标
        if (event.data.type === 'TEXT_SELECTED') {
          const mousePos = mousePositionRef.current
          const selectedText = event.data.text

          console.log('✓ 收到文本选中事件:', {
            text: selectedText,
            mousePosition: mousePos,
            timestamp: event.data.timestamp,
          })

          // 保存选中的文本到 state 并显示悬浮菜单(固定到屏幕中心)
          setContextMenu({
            visible: true,
            x: 0, // 不再使用鼠标坐标
            y: 0,
            selectedText: selectedText,
          })

          console.log('选中文本已保存到 state:', selectedText)
          console.log('悬浮菜单显示在坐标:', mousePos)
        }

        // 隐藏文本选中菜单消息
        if (event.data.type === 'HIDE_TEXT_SELECTED') {
          console.log('✓ 收到隐藏菜单事件:', {
            timestamp: event.data.timestamp,
          })

          setContextMenu({
            visible: false,
            x: 0,
            y: 0,
            selectedText: '',
          })

          console.log('悬浮菜单已隐藏')
        }
      }
    }

    window.addEventListener('message', handleMessage)
    return () => {
      window.removeEventListener('message', handleMessage)
    }
  }, [])

  // 监听全局点击事件,点击菜单外部时隐藏菜单
  useEffect(() => {
    if (!contextMenu.visible) return

    const handleClickOutside = (e: MouseEvent) => {
      const target = e.target as HTMLElement
      // 检查点击的元素是否在菜单内部
      const menuElement = document.getElementById('context-menu')
      if (menuElement && !menuElement.contains(target)) {
        setContextMenu((prev) => ({ ...prev, visible: false }))
      }
    }

    // 延迟添加事件监听,避免菜单刚显示就被关闭
    const timer = setTimeout(() => {
      document.addEventListener('click', handleClickOutside)
    }, 100)

    return () => {
      clearTimeout(timer)
      document.removeEventListener('click', handleClickOutside)
    }
  }, [contextMenu.visible])

  // 添加到对话
  const handleAddToChat = () => {
    console.log('添加到对话 - 选中的文本:', contextMenu.selectedText)
    setContextMenu((prev) => ({ ...prev, visible: false }))
  }

  // 翻译
  const handleTranslateText = () => {
    console.log('翻译 - 选中的文本:', contextMenu.selectedText)
    setContextMenu((prev) => ({ ...prev, visible: false }))
  }

  const handleTest = (type: any) => {
    setError(null)
    setIsPreviewReady(true)
    setIsUploading(false)
    setUploadedFile({})
    setTimeout(() => {
      setIsPreviewReady(true)
      initOnlyOfficeEditor({}, type)
    }, 500)
  }

  // 拖拽事件处理
  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault()
    setIsDragOver(true)
  }

  const handleDragLeave = () => {
    setIsDragOver(false)
  }

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault()
    setIsDragOver(false)

    const file = e.dataTransfer.files[0]
    if (file) {
      handleFileSelect(file)
    }
  }

  // 清除文件
  const handleClearFile = () => {
    setUploadedFile(null)
    setIsPreviewReady(false)
    setError(null)
    if (fileInputRef.current) {
      fileInputRef.current.value = ''
    }
  }

  return (
    <>
      {/* 加载 OnlyOffice API 脚本 */}
      <Script
        // src="https://demo.jdyos.com:7070/web-apps/apps/api/documents/api.js"
        src='http://localhost/web-apps/apps/api/documents/api.js'
        strategy='afterInteractive'
        onLoad={() => {
          console.log('OnlyOffice API 加载完成')
          setScriptLoaded(true)
        }}
        onError={(e) => {
          console.error('OnlyOffice API 加载失败:', e)
          setError('OnlyOffice API 加载失败,请检查网络连接或服务器配置')
        }}
      />

      <Header />

      <main className='m-only-office min-h-screen bg-gradient-to-br from-primary/5 via-background to-secondary/5 relative overflow-hidden'>
        {/* 背景装饰 */}
        <div className='absolute inset-0 overflow-hidden pointer-events-none'>
          <div className='absolute top-20 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse-slow' />
          <div
            className='absolute bottom-20 right-1/4 w-96 h-96 bg-secondary/5 rounded-full blur-3xl animate-pulse-slow'
            style={{ animationDelay: '2s' }}
          />
          <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-gradient-to-r from-primary/3 to-secondary/3 rounded-full blur-3xl animate-spin-slow' />
        </div>

        {/* 内容区域 */}
        <div className='relative max-w-7xl mx-auto px-4 py-8'>
          {/* 返回按钮 */}
          <button
            onClick={() => router.push('/light')}
            className='group mb-8 flex items-center gap-2 px-4 py-3 rounded-2xl bg-card/80 backdrop-blur-xl border-2 border-border hover:border-primary shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 animate-fade-in'
          >
            <div className='relative'>
              <div className='absolute inset-0 bg-primary/20 rounded-full blur-md scale-0 group-hover:scale-150 transition-transform duration-500' />
              <ArrowLeft className='relative w-5 h-5 text-primary group-hover:text-primary transition-all duration-300 group-hover:-translate-x-1' />
            </div>
            <span className='text-sm font-medium text-foreground group-hover:text-primary transition-colors duration-300'>
              返回
            </span>
          </button>

          {/* 主标题卡片 */}
          <div className='mb-8 p-8 rounded-3xl bg-gradient-to-br from-card/95 to-card/80 backdrop-blur-2xl border-2 border-border/50 shadow-2xl animate-fade-in'>
            <div className='flex items-center gap-4 mb-3'>
              <div className='relative'>
                <div className='absolute inset-0 bg-primary/20 rounded-2xl blur-xl animate-pulse-slow' />
                <div className='relative w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center shadow-lg'>
                  <FileText className='w-8 h-8 text-primary-foreground' />
                </div>
              </div>
              <div className='flex-1'>
                <h1 className='text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent'>
                  OnlyOffice 文档预览
                </h1>
                <p className='text-sm text-muted-foreground mt-1'>
                  支持 Word、Excel、PowerPoint 等多种格式的文档在线预览
                </p>
              </div>
            </div>

            {/* 支持格式说明 */}
            <div className='mt-4 p-4 rounded-xl bg-muted/30 border border-border/50'>
              <div className='flex items-center gap-2 mb-2'>
                <Sparkles className='w-4 h-4 text-primary' />
                <span className='text-sm font-semibold text-foreground'>
                  支持的文件格式
                </span>
              </div>
              <div className='grid grid-cols-1 md:grid-cols-3 gap-2 text-xs text-muted-foreground'>
                <div>
                  <span className='font-medium text-foreground'>文档:</span>
                  DOC, DOCX, ODT, RTF, TXT, PDF 等
                </div>
                <div>
                  <span className='font-medium text-foreground'>表格:</span>
                  XLS, XLSX, ODS, CSV 等
                </div>
                <div>
                  <span className='font-medium text-foreground'>演示:</span>
                  PPT, PPTX, ODP 等
                </div>
              </div>
            </div>
          </div>

          <div style={{ display: 'flex' }}>
            <button
              onClick={() => handleTest('1')}
              className='group mb-8 flex items-center gap-2 px-4 py-3 rounded-2xl bg-card/80 backdrop-blur-xl border-2 border-border hover:border-primary shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 animate-fade-in'
            >
              <span className='text-sm font-medium text-foreground group-hover:text-primary transition-colors duration-300'>
                测试1
              </span>
            </button>
            <button
              onClick={() => handleTest('2')}
              className='group mb-8 flex items-center gap-2 px-4 py-3 rounded-2xl bg-card/80 backdrop-blur-xl border-2 border-border hover:border-primary shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 animate-fade-in'
            >
              <span className='text-sm font-medium text-foreground group-hover:text-primary transition-colors duration-300'>
                测试2
              </span>
            </button>
            <button
              onClick={() => handleTest('3')}
              className='group mb-8 flex items-center gap-2 px-4 py-3 rounded-2xl bg-card/80 backdrop-blur-xl border-2 border-border hover:border-primary shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 animate-fade-in'
            >
              <span className='text-sm font-medium text-foreground group-hover:text-primary transition-colors duration-300'>
                测试3
              </span>
            </button>
          </div>

          {/* 上传区域或文件预览 */}
          {!uploadedFile ? (
            <div className='p-8 rounded-3xl bg-gradient-to-br from-card/95 to-card/80 backdrop-blur-2xl border-2 border-border/50 shadow-2xl animate-fade-in-up'>
              <div
                className={`
                  border-2 border-dashed rounded-2xl p-16 text-center cursor-pointer
                  transition-all duration-300 relative overflow-hidden
                  ${
                    isDragOver
                      ? 'border-primary bg-primary/10 scale-[1.02]'
                      : 'border-border hover:border-primary/50 hover:bg-muted/30'
                  }
                `}
                onClick={() => fileInputRef.current?.click()}
                onDragOver={handleDragOver}
                onDragLeave={handleDragLeave}
                onDrop={handleDrop}
              >
                {/* 装饰性背景 */}
                <div className='absolute inset-0 overflow-hidden pointer-events-none'>
                  <div className='absolute top-0 left-1/4 w-32 h-32 bg-primary/5 rounded-full blur-2xl animate-float' />
                  <div
                    className='absolute bottom-0 right-1/4 w-32 h-32 bg-secondary/5 rounded-full blur-2xl animate-float'
                    style={{ animationDelay: '1s' }}
                  />
                </div>

                <div className='relative flex flex-col items-center gap-6'>
                  <div
                    className={`
                      w-24 h-24 rounded-full flex items-center justify-center
                      transition-all duration-500
                      ${
                        isDragOver
                          ? 'bg-primary scale-110 shadow-2xl shadow-primary/50'
                          : 'bg-gradient-to-br from-primary/20 to-secondary/20 shadow-lg'
                      }
                    `}
                  >
                    {isUploading ? (
                      <Loader2 className='w-12 h-12 text-primary animate-spin' />
                    ) : (
                      <Upload
                        className={`w-12 h-12 transition-all duration-300 ${
                          isDragOver ? 'text-white scale-110' : 'text-primary'
                        }`}
                      />
                    )}
                  </div>

                  <div>
                    <div className='text-2xl font-bold text-foreground mb-2'>
                      {isUploading
                        ? '上传中...'
                        : isDragOver
                          ? '松开以上传文件'
                          : '点击或拖拽文件到此处'}
                    </div>
                    <div className='text-sm text-muted-foreground mb-4'>
                      支持 Word、Excel、PowerPoint、PDF 等多种格式
                    </div>
                    <div className='inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20'>
                      <FolderOpen className='w-4 h-4 text-primary' />
                      <span className='text-xs font-medium text-primary'>
                        单个文件,最大支持 100MB
                      </span>
                    </div>
                  </div>
                </div>
              </div>

              <input
                ref={fileInputRef}
                type='file'
                accept={ALL_FORMATS.join(',')}
                className='hidden'
                onChange={(e) => {
                  const file = e.target.files?.[0]
                  if (file) handleFileSelect(file)
                }}
              />

              {/* 错误提示 */}
              {error && (
                <div className='mt-6 p-4 rounded-xl bg-destructive/10 border-2 border-destructive/30 flex items-start gap-3 animate-shake'>
                  <AlertCircle className='w-5 h-5 text-destructive mt-0.5 flex-shrink-0' />
                  <div className='flex-1'>
                    <div className='font-semibold text-destructive mb-1'>
                      上传失败
                    </div>
                    <div className='text-sm text-destructive/80'>{error}</div>
                  </div>
                </div>
              )}
            </div>
          ) : (
            <>
              {/* 文件信息卡片 */}
              <div className='mb-6 p-6 rounded-3xl bg-gradient-to-br from-card/95 to-card/80 backdrop-blur-2xl border-2 border-primary/30 shadow-2xl animate-fade-in'>
                <div className='flex items-center justify-between'>
                  <div className='flex items-center gap-4 flex-1'>
                    <div className='relative'>
                      <div className='absolute inset-0 bg-green-500/20 rounded-xl blur-lg animate-pulse-slow' />
                      <div className='relative w-12 h-12 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center shadow-lg'>
                        <File className='w-6 h-6 text-white' />
                      </div>
                    </div>
                    <div className='flex-1 min-w-0'>
                      <div className='flex items-center gap-2 mb-1'>
                        <h3 className='text-lg font-semibold text-foreground truncate'>
                          {uploadedFile.name}
                        </h3>
                        {isPreviewReady && (
                          <div className='flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/10 border border-green-500/30'>
                            <CheckCircle2 className='w-3 h-3 text-green-500' />
                            <span className='text-xs font-medium text-green-500'>
                              预览就绪
                            </span>
                          </div>
                        )}
                      </div>
                      <div className='flex items-center gap-3 text-xs text-muted-foreground'>
                        <span>{formatBytes(uploadedFile.size)}</span>
                        <span>•</span>
                        <span className='capitalize'>
                          {uploadedFile.type} 文档
                        </span>
                        <span>•</span>
                        <span>
                          {new Date(
                            uploadedFile.uploadTime,
                          ).toLocaleTimeString()}
                        </span>
                      </div>
                    </div>
                  </div>

                  <Button
                    onClick={handleClearFile}
                    variant='outline'
                    size='sm'
                    className='rounded-xl border-2 hover:border-destructive hover:bg-destructive/5 hover:text-destructive transition-all duration-300'
                  >
                    <X className='w-4 h-4 mr-1' />
                    关闭
                  </Button>
                </div>
              </div>

              {/* 预览区域 */}
              <div className='rounded-3xl bg-gradient-to-br from-card/95 to-card/80 backdrop-blur-2xl border-2 border-border/50 shadow-2xl animate-fade-in-up overflow-hidden'>
                {!isPreviewReady ? (
                  <div className='w-full h-[calc(100vh-300px)] min-h-[700px] flex flex-col items-center justify-center gap-4 bg-muted/20'>
                    <Loader2 className='w-12 h-12 text-primary animate-spin' />
                    <div className='text-lg font-semibold text-foreground'>
                      正在加载预览...
                    </div>
                    <div className='text-sm text-muted-foreground'>
                      请稍候片刻
                    </div>
                  </div>
                ) : (
                  <div className='relative'>
                    <div
                      ref={editorContainerRef}
                      id='onlyoffice-editor'
                      className='w-full h-[calc(100vh-300px)] min-h-[700px] bg-white'
                    >
                      {/* OnlyOffice 编辑器将在这里加载 */}
                    </div>
                  </div>
                )}
              </div>
            </>
          )}
        </div>
      </main>

      {/* 上下文菜单 */}
      {contextMenu.visible && (
        <div
          id='context-menu'
          className='fixed z-50'
          style={{
            left: '50%',
            top: '50%',
            transform: 'translate(-50%, -50%)',
          }}
        >
          <div className='px-4 py-3 rounded-xl bg-gradient-to-br from-card/95 to-card/90 backdrop-blur-2xl border-2 border-border/50 shadow-2xl'>
            <div className='flex items-center gap-2'>
              {/* 添加到对话按钮 */}
              <button
                onClick={handleAddToChat}
                className='group relative px-4 py-2.5 rounded-lg hover:bg-primary/10 transition-all duration-200 flex items-center gap-2'
                title='添加到对话'
              >
                <div className='absolute inset-0 bg-primary/20 rounded-lg blur-md scale-0 group-hover:scale-100 transition-transform duration-300' />
                <MessageSquare className='relative w-4 h-4 text-foreground group-hover:text-primary transition-colors duration-200' />
                <span className='relative text-sm text-foreground group-hover:text-primary transition-colors duration-200'>
                  添加到对话
                </span>
              </button>

              {/* 翻译按钮 */}
              <button
                onClick={handleTranslateText}
                className='group relative px-4 py-2.5 rounded-lg hover:bg-primary/10 transition-all duration-200 flex items-center gap-2'
                title='翻译'
              >
                <div className='absolute inset-0 bg-primary/20 rounded-lg blur-md scale-0 group-hover:scale-100 transition-transform duration-300' />
                <BookOpen className='relative w-4 h-4 text-foreground group-hover:text-primary transition-colors duration-200' />
                <span className='relative text-sm text-foreground group-hover:text-primary transition-colors duration-200'>
                  翻译
                </span>
              </button>
            </div>
          </div>
        </div>
      )}

      {/* 自定义动画样式 */}
      <style jsx global>{`
        @keyframes pulse-slow {
          0%,
          100% {
            opacity: 0.3;
            transform: scale(1);
          }
          50% {
            opacity: 0.6;
            transform: scale(1.05);
          }
        }

        @keyframes spin-slow {
          from {
            transform: translate(-50%, -50%) rotate(0deg);
          }
          to {
            transform: translate(-50%, -50%) rotate(360deg);
          }
        }

        @keyframes fade-in {
          from {
            opacity: 0;
            transform: translateY(10px);
          }
          to {
            opacity: 1;
            transform: translateY(0);
          }
        }

        @keyframes fade-in-up {
          from {
            opacity: 0;
            transform: translateY(20px);
          }
          to {
            opacity: 1;
            transform: translateY(0);
          }
        }

        @keyframes float {
          0%,
          100% {
            transform: translateY(0px);
          }
          50% {
            transform: translateY(-20px);
          }
        }

        @keyframes shake {
          0%,
          100% {
            transform: translateX(0);
          }
          10%,
          30%,
          50%,
          70%,
          90% {
            transform: translateX(-2px);
          }
          20%,
          40%,
          60%,
          80% {
            transform: translateX(2px);
          }
        }

        .animate-pulse-slow {
          animation: pulse-slow 4s ease-in-out infinite;
        }

        .animate-spin-slow {
          animation: spin-slow 20s linear infinite;
        }

        .animate-fade-in {
          animation: fade-in 0.5s ease-out forwards;
        }

        .animate-fade-in-up {
          animation: fade-in-up 0.6s ease-out forwards;
        }

        .animate-float {
          animation: float 3s ease-in-out infinite;
        }

        .animate-shake {
          animation: shake 0.5s ease-in-out;
        }

        /* OnlyOffice 编辑器 iframe 样式 */
        .m-only-office iframe {
          width: 100% !important;
          height: 100% !important;
          min-height: 700px !important;
          border: none !important;
        }
      `}</style>
    </>
  )
}
相关推荐
Never_Satisfied2 小时前
C#数组去重方法总结
开发语言·c#
阿蒙Amon2 小时前
C#每日面试题-静态构造函数和普通构造函数区别
java·开发语言·c#
Java程序员威哥2 小时前
SpringBoot4.0+JDK25+GraalVM:云原生Java的性能革命与落地指南
java·开发语言·后端·python·云原生·c#
23124_802 小时前
Base64多层嵌套解码
前端·javascript·数据库
liu_sir_2 小时前
android9.0 amlogic 遥控器POWER按键的假待机的实现
开发语言·git·python
少控科技2 小时前
QT高阶日记5
开发语言·qt
froginwe112 小时前
Swift 数组
开发语言
多看书少吃饭2 小时前
文件预览的正确做法:从第三方依赖到企业级自建方案(Vue + Java 实战)
java·前端·vue.js
菜鸟很沉2 小时前
Vue3 + Element Plus 实现大文件分片上传组件(支持秒传、断点续传)
javascript·vue.js