13 KiB
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 关键迁移点
-
DOM 操作 → 模板渲染
-
主容器 HTML 使用 Vue 模板
-
文件列表使用
v-for渲染 -
条件渲染使用
v-if/v-show
-
-
事件处理
-
选择文件:
@change -
拖拽:
@dragenter,@dragover,@dragleave,@drop -
粘贴:
@paste -
按钮点击:
@click
-
-
响应式数据
-
files→ref([]) -
selectedFiles→ref(new Set()) -
actionMenuState→reactive({}) -
tasks→ref([])
-
-
生命周期
-
init()→onMounted() -
destroy()→onUnmounted()
-
-
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. 测试验证
迁移完成后需要验证:
- 文件列表显示正常
- 文件选择功能正常(点击、拖拽、粘贴)
- 文件上传功能正常
- 文件下载功能正常(单个和批量)
- 文件删除功能正常
- 任务面板显示正常
- 通知系统正常
- 权限控制正常
- 多语言支持正常
7. 潜在风险
- jQuery 插件封装:原代码使用
$.fn.G3File封装,迁移后需要提供 Vue 组件注册方式 - DOM 引用:需要确保所有模板 ref 正确绑定
- 全局事件:需要在
onUnmounted中正确清理 - 异步操作:确保所有异步操作(如文件上传、下载)正确处理
8. 输出文件
-
custom-file-vue/index.js- Vue 3 组件实现 -
custom-file-vue/index.css- 样式文件(保持不变)