u
This commit is contained in:
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` - 样式文件(保持不变)
|
||||
|
||||
Reference in new issue
Block a user