# 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 核心数据结构 ```javascript // 文件对象 { id: string, name: string, size: number, uploadDate: string, url: string, file: File, status: 'pending' | 'uploading' | 'uploaded' | 'failed' } // 选中文件集合 selectedFiles: Set // 任务状态 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 组件结构 ```javascript 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:模板结构 ```html
{{ language.pending }}
{{ staticIconMap.delete }}
{{ file.name }} {{ formatFileSize(file.size) }} {{ formatDate(file.uploadDate) }}
{{ staticIconMap.preview }}
{{ staticIconMap.download }}
{{ staticIconMap.more }}
{{ language.emptyTip1 }} {{ language.emptyTip2 }} {{ language.emptyTip3 }}
{{ item.title }}
Task Running
{{ taskStats.success + taskStats.fail }} / {{ taskStats.total }} {{ Math.round(totalProgress) }}%
{{ currentTask?.name }}
{{ language.taskTotal }}: {{ taskStats.total }} | {{ language.taskSuccess }}: {{ taskStats.success }} | {{ language.taskFail }}: {{ taskStats.fail }}
{{ notification.title }}
{{ notification.message }}
``` ### 步骤 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:权限控制迁移 保持原有的权限判断逻辑: ```javascript 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` - 样式文件(保持不变)