Files
workspace/code/file-preview/.trae/documents/g3file_vue_migration_plan.md
T
2026-04-28 17:29:53 +08:00

13 KiB
Raw Blame History

G3File jQuery 到 Vue 3 迁移计划

1. 需求分析

用户需要将 custom-file/index.js 中的 jQuery 代码迁移到 custom-file-vue/index.js,使用 Vue 3 的 vue.global.prod.js。要求:

  • 代码逻辑不变

  • CSS 代码不变

2. 原 jQuery 代码结构分析

2.1 主要功能模块

模块 功能描述
配置与常量 默认配置、多语言、图标映射
工具方法 文件图标、图片压缩、大小格式化、日期格式化
UI 渲染 主容器、文件列表、操作菜单、任务面板、通知
事件绑定 文件选择、拖拽、粘贴、按钮点击
文件处理 选择、下载、删除、上传
任务管理 批量下载/上传进度跟踪
通知系统 消息提示
权限控制 全局权限和单文件权限

2.2 核心数据结构

// 文件对象
{
  id: string,
  name: string,
  size: number,
  uploadDate: string,
  url: string,
  file: File,
  status: 'pending' | 'uploading' | 'uploaded' | 'failed'
}

// 选中文件集合
selectedFiles: Set<string>

// 任务状态
taskStats: { total, success, fail }

3. Vue 3 迁移方案

3.1 组件结构

G3File.vue (主组件)
  ├── 模板:HTML 结构
  ├── 脚本:响应式数据和方法
  └── 样式:引用原 CSS

3.2 迁移策略

jQuery 特性 Vue 3 等价实现
$(selector) ref + template refs
.html() v-html / 模板插值
.on('event') @event 指令
.off('event') Vue 自动管理
$.extend() 展开运算符
$.fn 插件 Vue 组件

3.3 关键迁移点

  1. DOM 操作 → 模板渲染

    • 主容器 HTML 使用 Vue 模板

    • 文件列表使用 v-for 渲染

    • 条件渲染使用 v-if/v-show

  2. 事件处理

    • 选择文件:@change

    • 拖拽:@dragenter, @dragover, @dragleave, @drop

    • 粘贴:@paste

    • 按钮点击:@click

  3. 响应式数据

    • files → ref([])

    • selectedFiles → ref(new Set())

    • actionMenuState → reactive({})

    • tasks → ref([])

  4. 生命周期

    • init() → onMounted()

    • destroy() → onUnmounted()

  5. Setup写法

4. 迁移步骤

步骤 1:创建 Vue 组件结构

const { createApp, ref, reactive, onMounted, onUnmounted, computed } = Vue

const G3File = {
  template: `...`,
  props: {
    options: { type: Object, default: () => ({}) }
  },
  setup(props) {
    // 响应式数据
    const files = ref([])
    const selectedFiles = ref(new Set())
    const objectUrls = ref(new Map())
    const actionMenuState = reactive({ open: false, fileId: null, anchorEl: null, items: [] })
    const tasks = ref([])
    const taskStats = reactive({ total: 0, success: 0, fail: 0 })
    const notifications = ref([])
    
    // 计算属性
    const pendingCount = computed(() => files.value.filter(f => f.status === 'pending').length)
    const allSelected = computed(() => files.value.length > 0 && selectedFiles.value.size === files.value.length)
    
    // 方法
    const generateUniqueId = () => 'file_' + Math.random().toString(36).slice(2) + Date.now()
    const getFileIcon = (filename) => { /* ... */ }
    const compressImage = async (file, options) => { /* ... */ }
    const isImageFile = (filename) => { /* ... */ }
    const formatFileSize = (bytes) => { /* ... */ }
    const formatDate = (dateStr) => { /* ... */ }
    const getUniqueFileName = (fileName, existing) => { /* ... */ }
    const getFileBlob = async (url) => { /* ... */ }
    
    // UI 更新方法
    const updateUI = () => { /* ... */ }
    const renderFileList = () => { /* ... */ }
    
    // 文件操作方法
    const handleFileSelect = async (event) => { /* ... */ }
    const handleDownload = (fileList, options) => { /* ... */ }
    const handleDelete = (fileList) => { /* ... */ }
    const handleUpload = (fileList) => { /* ... */ }
    
    // 任务管理
    const addTask = (task) => { /* ... */ }
    const updateTask = (taskId, updates) => { /* ... */ }
    const finishTask = (taskId, status) => { /* ... */ }
    const clearTasks = () => { /* ... */ }
    
    // 通知系统
    const showNotification = (options) => { /* ... */ }
    const closeNotification = (notificationId) => { /* ... */ }
    
    // 权限控制
    const hasPermission = (file, action) => { /* ... */ }
    
    // 生命周期
    onMounted(() => {
      // 初始化脚本加载
      // 绑定全局事件(拖拽、粘贴)
    })
    
    onUnmounted(() => {
      // 清理 ObjectURL
      // 解绑全局事件
    })
    
    return {
      files,
      selectedFiles,
      actionMenuState,
      tasks,
      taskStats,
      notifications,
      pendingCount,
      allSelected,
      // ... methods
    }
  }
}

步骤 2:模板结构

<div class="g3-file-container" :class="{ 'drag-over': isDragOver }">
  <!-- 头部 -->
  <div class="header">
    <label class="check-all">
      <input type="checkbox" v-model="allSelected" />
      <span>{{ language.selectAll }}</span>
    </label>
    <button class="download-all" :disabled="!canBatchDownload" @click="handleBatchDownload">
      {{ staticIconMap.download }}
      {{ language.downloadBatch }}
    </button>
    <button class="delete-all" :disabled="!canBatchDelete" @click="handleBatchDelete">
      {{ staticIconMap.delete }}
      {{ language.delete }}
    </button>
  </div>
  
  <!-- 文件列表 -->
  <div class="main" v-show="files.length > 0">
    <div 
      v-for="file in files" 
      :key="file.id" 
      class="g3-file-item"
      @click="handleFileClick(file)"
    >
      <input type="checkbox" v-model="selectedFiles.has(file.id)" />
      <label class="g3-checkbox-label"></label>
      <div class="g3-file-preview">
        <!-- 图片或图标 -->
      </div>
      <div v-if="file.status === 'pending'" class="g3-file-pending-badge">
        {{ language.pending }}
      </div>
      <div class="g3-file-delete-btn" @click.stop="handleDelete([file])">
        {{ staticIconMap.delete }}
      </div>
      <div class="g3-file-info">
        <span class="g3-file-name">{{ file.name }}</span>
        <span>{{ formatFileSize(file.size) }}</span>
        <span>{{ formatDate(file.uploadDate) }}</span>
      </div>
      <div class="g3-file-actions">
        <div class="g3-file-action preview" @click.stop="handlePreview(file)">
          {{ staticIconMap.preview }}
        </div>
        <div class="g3-file-action download" @click.stop="handleDownload([file], { batch: false })">
          {{ staticIconMap.download }}
        </div>
        <div class="g3-file-action more" @click.stop="openActionMenu($event, file)">
          {{ staticIconMap.more }}
        </div>
      </div>
    </div>
  </div>
  
  <!-- 空状态 -->
  <div class="empty-tip" v-show="files.length === 0">
    <span>{{ language.emptyTip1 }}</span>
    <span>{{ language.emptyTip2 }}</span>
    <span>{{ language.emptyTip3 }}</span>
  </div>
  
  <!-- 底部 -->
  <div class="footer">
    <div class="pending-count">
      <span>{{ language.pending }}</span>
      <span class="pending-number">{{ pendingCount }}</span>
    </div>
    <div>
      <button class="select" :disabled="!hasPermission(null, 'select')" @click="triggerFileInput">
        {{ staticIconMap.select }}
        {{ language.select }}
      </button>
      <button class="upload" :disabled="!hasPermission(null, 'upload') || pendingCount === 0" @click="handleUpload(files)">
        {{ staticIconMap.upload }}
        {{ language.upload }}
      </button>
    </div>
    <input type="file" class="file-input" style="display: none;" multiple @change="handleFileSelect" ref="fileInput" />
  </div>
  
  <!-- 操作菜单 -->
  <div class="g3-global-menu" :class="{ 'is-open': actionMenuState.open }">
    <div 
      v-for="(item, index) in actionMenuState.items" 
      :key="index"
      class="g3-action-menu-item"
      :class="{ 'is-disabled': item.disabled }"
      @click="handleActionMenuItemClick(item)"
    >
      {{ item.title }}
    </div>
  </div>
  
  <!-- 任务面板 -->
  <div class="task-dock" v-show="tasks.length > 0">
    <div class="dock-header">
      <span class="dock-title">Task Running</span>
      <div class="dock-actions">
        <button title="close" @click="clearTasks">×</button>
      </div>
    </div>
    <div class="dock-body">
      <div class="progress-line">
        <span class="count">{{ taskStats.success + taskStats.fail }} / {{ taskStats.total }}</span>
        <span class="percent">{{ Math.round(totalProgress) }}%</span>
      </div>
      <div class="task-name">{{ currentTask?.name }}</div>
      <div class="bar">
        <div class="bar-inner" :style="{ width: currentTask?.progress + '%' }"></div>
      </div>
      <div class="task-result">
        {{ language.taskTotal }}: {{ taskStats.total }} | 
        {{ language.taskSuccess }}: {{ taskStats.success }} | 
        {{ language.taskFail }}: {{ taskStats.fail }}
      </div>
    </div>
  </div>
  
  <!-- 通知容器 -->
  <div class="notification-container">
    <div 
      v-for="notification in notifications" 
      :key="notification.id"
      class="notification"
      :class="[`notification-${notification.type}`, { 'notification-fade-out': notification.closing }]"
    >
      <div class="notification-content">
        <div v-if="notification.title" class="notification-title">{{ notification.title }}</div>
        <div class="notification-message">{{ notification.message }}</div>
      </div>
      <button v-if="notification.showClose" class="notification-close" @click="closeNotification(notification.id)">×</button>
    </div>
  </div>
  
  <!-- UI Lock -->
  <div class="ui-lock" v-show="isUILocked"></div>
</div>

步骤 3:工具方法迁移

所有工具方法保持逻辑不变,只是移除 jQuery 依赖:

  • generateUniqueId - 保持不变

  • getFileIcon - 保持不变(纯字符串处理)

  • compressImage - 保持不变(使用原生 API)

  • isImageFile - 保持不变

  • formatFileSize - 保持不变

  • formatDate - 保持不变

  • getUniqueFileName - 保持不变

  • getFileBlob - 保持不变(使用 fetch API)

步骤 4:事件处理迁移

jQuery 事件 Vue 事件
$(document).on('paste') 在 onMounted 中绑定原生事件
$(document).on('click.g3file_actionmenu') Vue 模板事件
$container.on('dragenter dragover dragleave drop') @dragenter, @dragover, @dragleave, @drop
$fileInput.on('change') @change

步骤 5:权限控制迁移

保持原有的权限判断逻辑:

const hasPermission = (file, action) => {
  const globalPermission = props.options.globalPermission
  const filePermissionResolver = props.options.filePermissionResolver
  
  if (typeof filePermissionResolver === 'function') {
    const result = filePermissionResolver({ file, action })
    if (result !== undefined) return !!result
  }
  
  if (typeof globalPermission === 'function') {
    return !!globalPermission({ file, action })
  } else if (typeof globalPermission === 'object') {
    return !!globalPermission[action]
  }
  
  return true
}

5. 文件复制

由于 CSS 代码不需要改变,需要将 custom-file/index.css 复制到 custom-file-vue/index.css(如果不存在差异)。

6. 测试验证

迁移完成后需要验证:

  1. 文件列表显示正常
  2. 文件选择功能正常(点击、拖拽、粘贴)
  3. 文件上传功能正常
  4. 文件下载功能正常(单个和批量)
  5. 文件删除功能正常
  6. 任务面板显示正常
  7. 通知系统正常
  8. 权限控制正常
  9. 多语言支持正常

7. 潜在风险

  1. jQuery 插件封装:原代码使用 $.fn.G3File 封装,迁移后需要提供 Vue 组件注册方式
  2. DOM 引用:需要确保所有模板 ref 正确绑定
  3. 全局事件:需要在 onUnmounted 中正确清理
  4. 异步操作:确保所有异步操作(如文件上传、下载)正确处理

8. 输出文件

  • custom-file-vue/index.js - Vue 3 组件实现

  • custom-file-vue/index.css - 样式文件(保持不变)