This commit is contained in:
oneao committed 2026-04-28 17:29:53 +08:00
1 parent ae6ccad238
commit b2d113db38
17 files changed
+3087 -1630

No files matched your search

@@ -0,0 +1,418 @@
# 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<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 组件结构
```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
<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:权限控制迁移
保持原有的权限判断逻辑:
```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` - 样式文件(保持不变)