Commit 787c89f3 by 王路

初始化

parents
# Bug修复说明
## 问题描述
**Bug**: 点击单个文件"开始"下载时,会下载所有文件
## 问题原因
`startDownload` 方法的 `finally` 块中,当单个文件下载完成时,会调用 `startNextPendingDownload()` 方法,该方法会自动启动其他等待中的下载任务。
```javascript
// 问题代码
finally {
activeDownloadCount.value--
// 这里会启动其他等待中的任务
if (!isBatchDownloading.value) {
startNextPendingDownload()
}
}
```
## 修复方案
### 1. 添加单个下载标志
在下载store中添加 `isSingleDownloading` 标志来区分单个下载和批量下载:
```javascript
const isSingleDownloading = ref(false) // 是否正在单个下载
```
### 2. 修改自动启动逻辑
修改 `startNextPendingDownload` 方法,在单个下载时也不自动启动其他任务:
```javascript
const startNextPendingDownload = () => {
// 如果正在批量下载或单个下载,不自动启动其他任务
if (isBatchDownloading.value || isSingleDownloading.value) return
const pendingDownload = downloads.value.find(d => d.status === 'pending')
if (pendingDownload && activeDownloadCount.value < maxConcurrentDownloads.value) {
startDownload(pendingDownload.id)
}
}
```
### 3. 修改单个下载调用
创建专门的单个下载方法,设置单个下载标志:
```javascript
// 单个文件下载
const startSingleDownload = async (downloadId) => {
downloadStore.isSingleDownloading = true
try {
await downloadStore.startDownload(downloadId)
} finally {
downloadStore.isSingleDownloading = false
}
}
// 单个文件恢复下载
const resumeSingleDownload = async (downloadId) => {
downloadStore.isSingleDownloading = true
try {
await downloadStore.resumeDownload(downloadId)
} finally {
downloadStore.isSingleDownloading = false
}
}
```
### 4. 更新按钮事件
将表格中的单个下载按钮事件改为调用新的方法:
```javascript
// 开始按钮
@click="startSingleDownload(row.id)"
// 继续按钮
@click="resumeSingleDownload(row.id)"
// 重试按钮
@click="startSingleDownload(row.id)"
```
## 修复后的行为
### ✅ 正确的行为
- **单个下载**:点击单个文件的"开始"按钮,只下载该文件
- **批量下载**:点击"批量开始选中"或"下载当前页",只下载选中的文件
- **自动启动**:只有在非批量下载且非单个下载时,才会自动启动其他等待中的任务
### ❌ 错误的行为(已修复)
- 点击单个文件"开始"按钮,所有等待中的文件都开始下载
## 测试方法
### 测试单个下载
1. 上传Excel文件,解析添加多个下载任务
2. 只点击其中一个任务的"开始"按钮
3. **验证结果**:只有该任务开始下载,其他任务保持"等待"状态
### 测试批量下载
1. 选择多个任务
2. 点击"批量开始选中"按钮
3. **验证结果**:只有选中的任务开始下载
### 测试自动启动
1. 确保没有进行批量或单个下载操作
2. 手动开始一个下载任务
3. **验证结果**:其他等待中的任务可能会自动开始(这是正常的多线程下载行为)
## 技术细节
### 下载模式区分
- **单个下载模式**`isSingleDownloading = true`
- **批量下载模式**`isBatchDownloading = true`
- **自动启动模式**:两个标志都为 `false`
### 标志管理
- 单个下载:在下载开始时设置 `isSingleDownloading = true`,下载完成后重置
- 批量下载:在批量操作开始时设置 `isBatchDownloading = true`,批量操作完成后重置
- 自动启动:只有在两个标志都为 `false` 时才允许自动启动其他任务
## 影响范围
### 修复的功能
- ✅ 单个文件下载
- ✅ 单个文件恢复下载
- ✅ 单个文件重试下载
- ✅ 批量下载(不受影响)
- ✅ 自动启动(在适当时候仍然工作)
### 不受影响的功能
- 暂停下载
- 删除下载
- 状态筛选
- 分页显示
- 断点续传
## 验证清单
- [ ] 单个文件"开始"按钮只下载该文件
- [ ] 单个文件"继续"按钮只恢复该文件
- [ ] 单个文件"重试"按钮只重试该文件
- [ ] 批量下载功能正常工作
- [ ] 自动启动功能在适当时候仍然工作
- [ ] 其他功能不受影响
# 删除Bug修复说明
## 问题描述
**Bug**: 点击删除单个文件时触发"批量下载"
## 问题原因
`cancelDownload` 方法中,当删除一个正在下载的文件时:
1. 会减少 `activeDownloadCount`
2. 然后在 `finally` 块中调用 `startNextPendingDownload()`
3. 该方法会自动启动其他等待中的下载任务
4. 这导致删除操作意外触发了"批量下载"行为
```javascript
// 问题代码
const cancelDownload = (downloadId) => {
// ... 删除逻辑
if (download.status === 'downloading') {
activeDownloadCount.value-- // 减少活跃下载数
}
// ... 删除任务
// 这里会启动其他等待中的任务
if (!isBatchDownloading.value && !isSingleDownloading.value) {
startNextPendingDownload()
}
}
```
## 修复方案
### 1. 修改单个删除逻辑
删除操作不应该触发自动启动其他下载任务:
```javascript
const cancelDownload = (downloadId) => {
const download = downloads.value.find(d => d.id === downloadId)
if (download && download.controller) {
download.controller.abort()
if (download.status === 'downloading') {
activeDownloadCount.value--
}
}
const index = downloads.value.findIndex(d => d.id === downloadId)
if (index > -1) {
downloads.value.splice(index, 1)
}
// 清除本地存储
localStorage.removeItem(`download_${downloadId}`)
// 删除操作不触发自动启动其他下载任务
// 删除操作应该保持当前状态,不自动启动新任务
}
```
### 2. 修改批量删除逻辑
批量删除时设置标志,防止自动启动:
```javascript
const batchDeleteDownloads = (downloadIds) => {
// 设置批量操作标志,防止自动启动
isBatchDownloading.value = true
try {
downloadIds.forEach(id => cancelDownload(id))
} finally {
isBatchDownloading.value = false
}
}
```
## 修复后的行为
### ✅ 正确的行为
- **单个删除**:点击单个文件的"删除"按钮,只删除该文件,不启动其他下载
- **批量删除**:点击"批量删除选中"按钮,只删除选中的文件,不启动其他下载
- **清除已完成**:只清除已完成的任务,不影响其他任务
- **清除所有**:清除所有任务,重置状态
### ❌ 错误的行为(已修复)
- 点击删除单个文件时,其他等待中的文件开始下载
- 删除操作意外触发"批量下载"行为
## 测试方法
### 测试单个删除
1. 上传Excel文件,解析添加多个下载任务
2. 开始下载几个任务
3. 点击其中一个任务的"删除"按钮
4. **验证结果**:只删除该任务,其他任务保持当前状态
### 测试批量删除
1. 选择多个任务
2. 点击"批量删除选中"按钮
3. **验证结果**:只删除选中的任务,其他任务保持当前状态
### 测试删除正在下载的任务
1. 开始下载一个任务
2. 在下载过程中点击"删除"按钮
3. **验证结果**:任务被删除,不会启动其他等待中的任务
## 技术细节
### 删除操作的特点
- **不触发自动启动**:删除操作应该保持当前状态
- **清理资源**:正确清理下载控制器和本地存储
- **更新计数**:正确更新活跃下载计数
### 标志管理
- **单个删除**:不设置任何标志,直接删除
- **批量删除**:设置 `isBatchDownloading = true`,防止自动启动
- **清除操作**:直接操作数组,不涉及自动启动逻辑
## 影响范围
### 修复的功能
- ✅ 单个文件删除
- ✅ 批量文件删除
- ✅ 删除正在下载的文件
- ✅ 清除已完成任务
- ✅ 清除所有任务
### 不受影响的功能
- 单个文件下载
- 批量文件下载
- 暂停和恢复
- 状态筛选
- 分页显示
## 验证清单
- [ ] 单个文件删除不触发其他下载
- [ ] 批量删除不触发其他下载
- [ ] 删除正在下载的文件正常工作
- [ ] 清除已完成任务正常工作
- [ ] 清除所有任务正常工作
- [ ] 其他功能不受影响
## 设计原则
### 删除操作的设计原则
1. **静默操作**:删除操作不应该影响其他任务的状态
2. **资源清理**:确保正确清理所有相关资源
3. **状态一致性**:保持下载列表状态的一致性
4. **用户预期**:删除操作应该符合用户的预期行为
### 自动启动的设计原则
1. **明确触发**:只有在明确需要时才自动启动
2. **状态检查**:在自动启动前检查当前状态
3. **标志控制**:使用标志控制自动启动行为
4. **用户控制**:用户的操作应该优先于自动行为
# 功能更新说明
## 新增功能
### 1. 状态筛选功能
- **位置**:下载任务管理页面顶部
- **功能**:可以按下载状态筛选显示的任务
- **筛选选项**
- 全部
- 等待中
- 下载中
- 已暂停
- 已完成
- 失败
### 2. 智能操作控制
根据任务状态,只有特定状态的任务可以执行相应操作:
#### 开始下载
- **可操作状态**:等待中、已暂停、失败
- **按钮显示**:状态为"等待中"时显示"开始"按钮
- **批量操作**:只对可开始下载的任务生效
#### 暂停下载
- **可操作状态**:仅下载中
- **按钮显示**:状态为"下载中"时显示"暂停"按钮
- **批量操作**:只对正在下载的任务生效
#### 继续下载
- **可操作状态**:仅已暂停
- **按钮显示**:状态为"已暂停"时显示"继续"按钮
- **功能**:支持断点续传
#### 重试下载
- **可操作状态**:仅失败
- **按钮显示**:状态为"失败"时显示"重试"按钮
#### 删除任务
- **可操作状态**:所有状态
- **按钮显示**:始终显示"删除"按钮
## 操作逻辑
### 批量开始选中
1. 检查选中的任务
2. 过滤出可以开始下载的任务(等待中、已暂停、失败)
3. 如果过滤后没有可操作任务,显示警告
4. 只对可操作的任务执行下载
### 批量暂停选中
1. 检查选中的任务
2. 过滤出正在下载的任务
3. 如果过滤后没有可操作任务,显示警告
4. 只对可操作的任务执行暂停
### 下载当前页
1. 获取当前页所有任务
2. 过滤出可以开始下载的任务
3. 如果过滤后没有可操作任务,显示警告
4. 只对可操作的任务执行下载
### 暂停当前页
1. 获取当前页所有任务
2. 过滤出正在下载的任务
3. 如果过滤后没有可操作任务,显示警告
4. 只对可操作的任务执行暂停
## 状态说明
### 等待中 (pending)
- 任务已添加但未开始下载
- 可以执行:开始下载、删除
### 下载中 (downloading)
- 任务正在下载中
- 可以执行:暂停、删除
- 显示下载进度和速度
### 已暂停 (paused)
- 任务已暂停,支持断点续传
- 可以执行:继续下载、删除
- 保留已下载的进度
### 已完成 (completed)
- 任务下载完成
- 可以执行:删除
- 显示100%进度
### 失败 (error)
- 任务下载失败
- 可以执行:重试、删除
- 显示错误信息
## 使用示例
### 场景1:筛选查看失败的任务
1. 在状态筛选下拉框中选择"失败"
2. 页面只显示失败的任务
3. 可以批量选择失败任务进行重试
### 场景2:暂停所有正在下载的任务
1. 在状态筛选下拉框中选择"下载中"
2. 页面只显示正在下载的任务
3. 点击"下载当前页"按钮暂停所有任务
### 场景3:批量重试失败的任务
1. 在状态筛选下拉框中选择"失败"
2. 选择需要重试的任务
3. 点击"批量开始选中"按钮
## 技术实现
### 状态筛选
```javascript
const filteredDownloads = computed(() => {
return downloadStore.downloads.filter(item => {
if (statusFilter.value) {
return item.status === statusFilter.value
}
return true
})
})
```
### 智能操作过滤
```javascript
// 过滤可开始下载的任务
const startableItems = selectedItems.value.filter(item =>
['pending', 'paused', 'error'].includes(item.status)
)
// 过滤可暂停的任务
const pausableItems = selectedItems.value.filter(item =>
item.status === 'downloading'
)
```
### 分页适配
- 分页基于筛选后的数据
- 筛选后自动回到第一页
- 总数显示筛选后的数量
## 用户体验改进
1. **智能提示**:当没有可操作任务时,显示具体的警告信息
2. **状态一致性**:确保操作按钮与任务状态匹配
3. **批量操作优化**:只对符合条件的任务执行操作
4. **筛选功能**:快速定位特定状态的任务
5. **断点续传**:暂停的任务可以从中断点继续下载
## 浏览器兼容性
- 支持所有现代浏览器
- 状态筛选功能基于前端计算,无需后端支持
- 断点续传功能使用HTTP Range头部,兼容性好
# 文件下载器
一个基于Vue 3的现代化下载器应用,支持从Excel文件批量下载文件,具有类似迅雷的功能。
## 主要功能
- 🔐 **用户认证**: 登录系统,保护下载功能
- 📊 **Excel解析**: 支持灵活的Excel格式,可自定义文件名和URL列
- 📁 **文件名前缀**: 为所有下载文件添加自定义前缀
- 📄 **多列组合**: 支持多列组合生成文件名,用"-"分隔
- 📋 **分页显示**: 下载列表分页显示,默认每页10条
-**批量操作**: 支持批量开始、暂停、删除下载
- 🔄 **断点续传**: 支持暂停和恢复下载
- 🚀 **多线程下载**: 防止界面卡顿
- 📱 **响应式设计**: 适配不同屏幕尺寸
- 🔔 **下载通知**: 下载完成后通知用户
- 🎯 **按页下载**: 支持只下载当前页面的文件
- 🎛️ **状态筛选**: 按下载状态筛选文件
- 🛠️ **智能操作**: 根据文件状态智能显示操作按钮
## 技术栈
- **前端框架**: Vue 3 (Composition API)
- **状态管理**: Pinia
- **UI组件库**: Element Plus
- **路由管理**: Vue Router
- **Excel解析**: SheetJS (xlsx)
- **HTTP客户端**: XMLHttpRequest (支持断点续传)
- **样式**: CSS3 + Element Plus主题
## 安装和运行
### 环境要求
- Node.js 16+
- npm 或 yarn
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm run dev
```
### 构建生产版本
```bash
npm run build
```
## 使用说明
### 1. 用户登录
- 默认用户名: `admin`
- 默认密码: `123456`
### 2. Excel文件格式
支持任意格式的Excel文件,用户可自定义:
- **文件名列**: 可选择多列,用"-"连接
- **URL列**: 选择包含下载链接的列
### 3. 下载设置
- **文件名前缀**: 为所有下载文件添加前缀(如:`vue-downloader_文件名.pdf`
- **并发数**: 控制同时下载的文件数量
- **通知设置**: 下载完成后显示通知
### 4. 下载管理
- **单个操作**: 开始、暂停、恢复、删除单个文件
- **批量操作**: 批量开始、暂停、删除选中的文件
- **按页操作**: 下载或暂停当前页面的所有文件
- **状态筛选**: 按状态筛选文件(等待中、下载中、已暂停、已完成、错误)
### 5. 高级功能
- **断点续传**: 支持暂停后继续下载
- **自动重试**: 下载失败时自动重试
- **进度显示**: 实时显示下载进度和速度
- **文件扩展名**: 自动从URL提取文件扩展名
## 功能特性
### 文件名前缀
- 支持为所有下载文件添加自定义前缀
- 格式:`前缀_原文件名.扩展名`
- 示例:设置前缀为"vue-downloader",文件"document.pdf"将保存为"vue-downloader_document.pdf"
### 多线程下载
- 控制并发下载数量,防止界面卡顿
- 默认最大并发数:3个
- 可自定义调整并发数
### 断点续传
- 支持暂停下载后继续
- 使用HTTP Range头实现
- 自动保存下载进度
### 智能操作
- 只有"下载中"状态的文件可以暂停
- 只有"已暂停"状态的文件可以恢复
- "已完成"和"等待中"状态的文件不受影响
### 状态筛选
- 支持按状态筛选:等待中、下载中、已暂停、已完成、错误
- 筛选后分页显示
- 支持按页操作
## 浏览器兼容性
### 现代浏览器(Chrome 86+, Edge 86+)
- ✅ 支持File System Access API
- ✅ 自动文件名前缀
- ✅ 完整的下载功能
### 传统浏览器
- ✅ 降级到传统下载方法
- ✅ 文件名前缀功能
- ✅ 基本下载功能
## 项目结构
```
src/
├── components/ # 组件
├── views/ # 页面
│ ├── Login.vue # 登录页面
│ └── Downloader.vue # 下载器主页面
├── stores/ # 状态管理
│ ├── auth.js # 认证状态
│ └── download.js # 下载状态
├── router/ # 路由配置
│ └── index.js
├── style.css # 全局样式
└── main.js # 应用入口
```
## 开发说明
### 状态管理
使用Pinia进行状态管理:
- `auth` store: 管理用户认证状态
- `download` store: 管理下载任务和设置
### 下载逻辑
- 使用XMLHttpRequest实现断点续传
- 支持暂停、恢复、取消操作
- 自动重试机制
- 进度和速度计算
### 文件处理
- 使用SheetJS解析Excel文件
- 支持多种Excel格式
- 自动提取文件扩展名
## 许可证
MIT License
# 批量下载修复测试说明
## 问题描述
之前的版本在批量下载时会下载所有文件,而不是只下载选中的文件。
## 修复内容
1. 添加了 `isBatchDownloading` 标志来控制批量下载模式
2. 在批量下载期间,禁用自动启动其他等待中的下载任务
3. 确保只有选中的文件会被下载
## 测试步骤
### 1. 准备测试数据
1. 创建一个Excel文件,包含多行数据(建议10-20行)
2. 确保有文件名列和URL列
3. 上传Excel文件并解析
### 2. 测试选中下载功能
1. 在下载列表中,只选择2-3个任务(不要全选)
2. 点击"批量开始选中"按钮
3. **验证结果**:只有选中的任务开始下载,其他任务保持"等待"状态
### 3. 测试当前页下载功能
1. 确保当前页有多个任务
2. 点击"下载当前页"按钮
3. **验证结果**:只有当前页的任务开始下载,其他页面的任务保持"等待"状态
### 4. 测试分页功能
1. 切换到不同页面
2. 在不同页面选择不同的任务
3. 验证每个页面的批量操作只影响当前页
### 5. 测试文件路径
1. 设置自定义子文件夹为 "vue-downloader-files"
2. 开始下载
3. **验证结果**
- 现代浏览器:会提示保存到指定文件夹
- 传统浏览器:会显示提示信息要求手动保存到指定文件夹
## 预期行为
### ✅ 正确的行为
- 批量开始选中:只下载选中的任务
- 下载当前页:只下载当前页的任务
- 其他任务保持"等待"状态
- 文件保存到指定文件夹
### ❌ 错误的行为(已修复)
- 所有任务都开始下载
- 文件保存到默认位置
- 无法控制下载范围
## 技术实现
### 关键代码修改
```javascript
// 添加批量下载标志
const isBatchDownloading = ref(false)
// 在批量下载期间禁用自动启动
const startNextPendingDownload = () => {
if (isBatchDownloading.value) return
// ... 自动启动逻辑
}
// 批量下载方法
const batchStartDownloads = async (downloadIds) => {
isBatchDownloading.value = true
try {
// 只下载指定的任务
await Promise.all(downloadIds.map(id => startDownload(id)))
} finally {
isBatchDownloading.value = false
}
}
```
## 浏览器兼容性
### 现代浏览器(Chrome 86+, Edge 86+)
- 支持 File System Access API
- 可以直接选择保存位置
- 支持文件夹路径
### 传统浏览器
- 使用传统下载方法
- 显示提示信息
- 需要手动保存到指定文件夹
## 故障排除
### 如果仍然下载全部文件
1. 检查浏览器控制台是否有错误
2. 确认 `isBatchDownloading` 标志正常工作
3. 验证选中状态是否正确
### 如果文件路径不正确
1. 检查自定义子文件夹设置
2. 确认浏览器是否支持 File System Access API
3. 查看控制台提示信息
## 联系支持
如果测试中发现任何问题,请提供:
1. 浏览器版本
2. 控制台错误信息
3. 具体操作步骤
4. 预期结果和实际结果
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文件下载器 - 类似迅雷的下载工具</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
{
"name": "vue-downloader",
"version": "1.0.0",
"description": "A Vue-based downloader application similar to Thunder",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"pinia": "^2.1.6",
"axios": "^1.5.0",
"element-plus": "^2.3.9",
"xlsx": "^0.18.5",
"file-saver": "^2.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"vite": "^4.4.9",
"sass": "^1.66.1"
},
"keywords": [
"vue",
"downloader",
"thunder",
"file-download"
],
"author": "Your Name",
"license": "MIT",
"volta": {
"node": "22.18.0",
"npm": "10.9.3"
}
}
// Excel处理Worker
// 使用本地XLSX库
importScripts('./xlsx.full.min.js');
self.onmessage = function(e) {
try {
const { data, type, neededColumns } = e.data;
if (type === 'parseExcel') {
const result = parseExcelData(data, neededColumns);
self.postMessage({
type: 'success',
data: result
});
}
} catch (error) {
console.error('Worker error:', error);
self.postMessage({
type: 'error',
error: error.message || '未知错误'
});
}
};
function parseExcelData(arrayBuffer, neededColumns = null) {
try {
if (!arrayBuffer) {
throw new Error('没有接收到数据');
}
const data = new Uint8Array(arrayBuffer);
const workbook = XLSX.read(data, {
type: 'array',
cellDates: true,
cellNF: false,
cellStyles: false,
cellHTML: false,
cellFormula: false,
cellText: false
});
if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
throw new Error('Excel文件中没有找到工作表');
}
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
if (!worksheet) {
throw new Error('无法读取工作表数据');
}
// 限制最大行数,防止内存溢出
const maxRows = 100000; // 增加到10万行
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
if (range.e.r > maxRows) {
range.e.r = maxRows - 1;
worksheet['!ref'] = XLSX.utils.encode_range(range);
}
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
defval: '',
blankrows: false
});
if (!jsonData || jsonData.length === 0) {
throw new Error('Excel文件中没有数据');
}
// 转换为对象数组
const headers = jsonData[0];
if (!headers || headers.length === 0) {
throw new Error('Excel文件中没有列标题');
}
const rows = jsonData.slice(1).map(row => {
const obj = {};
headers.forEach((header, index) => {
obj[header] = row[index] || '';
});
return obj;
});
// 如果指定了需要的列,则只保留这些列
if (neededColumns && neededColumns.length > 0) {
return rows.map(row => {
const optimizedRow = {};
neededColumns.forEach(col => {
optimizedRow[col] = row[col] || '';
});
return optimizedRow;
});
}
return rows;
} catch (error) {
console.error('Excel解析错误:', error);
throw new Error('Excel解析失败: ' + error.message);
}
}
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from './stores/auth'
const authStore = useAuthStore()
onMounted(() => {
// 初始化认证状态
authStore.initialize()
})
</script>
<style>
#app {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
</style>
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
})
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import Login from '../views/Login.vue'
import Downloader from '../views/Downloader.vue'
import Settings from '../views/Settings.vue'
import History from '../views/History.vue'
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiresAuth: false }
},
{
path: '/downloader',
name: 'Downloader',
component: Downloader,
meta: { requiresAuth: true }
},
{
path: '/settings',
name: 'Settings',
component: Settings,
meta: { requiresAuth: true }
},
{
path: '/history',
name: 'History',
component: History,
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// 初始化认证状态
if (!authStore.isAuthenticated) {
authStore.initialize()
}
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next('/login')
} else if (to.path === '/login' && authStore.isAuthenticated) {
next('/downloader')
} else {
next()
}
})
export default router
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 400px;
text-align: center;
}
.login-title {
font-size: 28px;
font-weight: bold;
color: #333;
margin-bottom: 30px;
}
.downloader-container {
background: #f5f7fa;
min-height: 100vh;
}
.header {
background: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 0 20px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #409eff;
}
.main-content {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
/* 配置区域样式 */
.config-collapse {
margin-bottom: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.config-collapse .el-collapse-item__header {
background: #f8f9fa;
font-weight: 600;
font-size: 16px;
padding: 15px 20px;
}
.config-collapse .el-collapse-item__content {
padding: 20px;
}
.upload-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 下载列表主区域样式 */
.download-list-main {
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.download-header {
background: #f8f9fa;
padding: 20px;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.download-title {
display: flex;
flex-direction: column;
gap: 10px;
}
.download-title h2 {
display: flex;
align-items: center;
gap: 10px;
color: #333;
margin: 0;
}
.download-stats {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.download-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #909399;
}
.empty-state h3 {
margin: 20px 0 10px 0;
color: #606266;
}
.empty-state p {
color: #909399;
font-size: 14px;
}
/* 下载表格容器 */
.download-table-container {
padding: 0;
}
/* 文件信息样式 */
.file-info {
display: flex;
align-items: center;
gap: 10px;
}
.file-icon {
font-size: 20px;
color: #409eff;
flex-shrink: 0;
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
color: #333;
margin-bottom: 5px;
word-break: break-all;
}
.file-prefix {
color: #409eff;
font-weight: 600;
background: #ecf5ff;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
margin-right: 4px;
}
.excel-file-info {
display: flex;
align-items: center;
gap: 8px;
color: #606266;
}
.excel-file-name {
font-size: 13px;
font-weight: 500;
color: #409eff;
background: #f0f9ff;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #e1f3ff;
}
.file-url {
font-size: 12px;
color: #666;
word-break: break-all;
}
/* 进度容器样式 */
.progress-container {
display: flex;
flex-direction: column;
gap: 5px;
}
.speed-info {
font-size: 12px;
color: #666;
text-align: center;
}
.completed-text {
color: #67c23a;
font-weight: 500;
}
.pending-text {
color: #909399;
}
.size-info {
font-size: 12px;
color: #666;
}
/* 状态标签样式 */
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #f0f9ff;
color: #0369a1;
}
.status-downloading {
background: #f0fdf4;
color: #16a34a;
}
.status-completed {
background: #f0fdf4;
color: #16a34a;
}
.status-error {
background: #fef2f2;
color: #dc2626;
}
.status-paused {
background: #fef3c7;
color: #d97706;
}
/* 分页容器样式 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 20px;
background: #f8f9fa;
border-top: 1px solid #e4e7ed;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
padding: 10px;
}
.download-header {
flex-direction: column;
align-items: flex-start;
}
.download-actions {
width: 100%;
justify-content: flex-start;
}
.download-stats {
width: 100%;
justify-content: flex-start;
}
}
/* 表格响应式 */
@media (max-width: 1200px) {
.el-table {
font-size: 12px;
}
.file-name {
font-size: 13px;
}
.file-url {
font-size: 11px;
}
}
/* 动画效果 */
.el-table__row {
transition: all 0.3s ease;
}
.el-table__row:hover {
background-color: #f5f7fa !important;
}
/* 下载通知样式 */
.download-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: #67c23a;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
animation: slideIn 0.3s ease-out;
max-width: 300px;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-content {
display: flex;
align-items: center;
gap: 10px;
}
.notification-icon {
font-size: 20px;
}
.notification-message {
flex: 1;
font-size: 14px;
}
.notification-close {
cursor: pointer;
font-size: 18px;
font-weight: bold;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.notification-close:hover {
opacity: 1;
}
<template>
<div class="settings-container">
<!-- 头部 -->
<header class="header">
<div class="logo">
<el-icon><Setting /></el-icon>
个人设置
</div>
<el-button @click="goBack" type="primary" size="small">
返回下载器
</el-button>
</header>
<!-- 主要内容 -->
<div class="main-content">
<el-card class="settings-card">
<template #header>
<div class="card-header">
<span>用户信息</span>
</div>
</template>
<el-form :model="userInfo" label-width="120px">
<el-form-item label="用户名">
<el-input v-model="userInfo.username" disabled />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="userInfo.email" disabled />
</el-form-item>
<el-form-item label="注册时间">
<el-input v-model="userInfo.createdAt" disabled />
</el-form-item>
<el-form-item label="最后登录">
<el-input v-model="userInfo.lastLoginAt" disabled />
</el-form-item>
</el-form>
</el-card>
<el-card class="settings-card">
<template #header>
<div class="card-header">
<span>下载设置</span>
</div>
</template>
<el-form :model="downloadSettings" label-width="120px">
<el-form-item label="最大并发数">
<el-input-number
v-model="downloadSettings.maxConcurrentDownloads"
:min="1"
:max="10"
@change="saveSettings"
/>
<span class="form-tip">同时下载的最大文件数量</span>
</el-form-item>
<el-form-item label="文件名前缀">
<el-input
v-model="downloadSettings.fileNamePrefix"
placeholder="例如: vue-downloader"
@input="saveSettings"
/>
<span class="form-tip">为所有下载文件添加前缀</span>
</el-form-item>
<el-form-item label="下载通知">
<el-switch
v-model="downloadSettings.enableNotifications"
@change="saveSettings"
/>
<span class="form-tip">下载完成后显示通知</span>
</el-form-item>
</el-form>
</el-card>
<el-card class="settings-card">
<template #header>
<div class="card-header">
<span>数据管理</span>
</div>
</template>
<div class="data-management">
<div class="data-item">
<div class="data-info">
<h4>下载历史</h4>
<p>清除所有下载历史记录</p>
</div>
<el-button @click="clearHistory" type="danger" size="small">
清除历史
</el-button>
</div>
<div class="data-item">
<div class="data-info">
<h4>用户数据</h4>
<p>删除当前用户的所有数据</p>
</div>
<el-button @click="deleteUserData" type="danger" size="small">
删除数据
</el-button>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Setting } from '@element-plus/icons-vue'
import { useAuthStore } from '../stores/auth'
const router = useRouter()
const authStore = useAuthStore()
// 用户信息
const userInfo = reactive({
username: '',
email: '',
createdAt: '',
lastLoginAt: ''
})
// 下载设置
const downloadSettings = reactive({
maxConcurrentDownloads: 3,
fileNamePrefix: '',
enableNotifications: true
})
// 初始化数据
const initializeData = () => {
const user = authStore.user
if (user) {
userInfo.username = user.username
userInfo.email = user.email || '未设置'
userInfo.createdAt = new Date(user.createdAt).toLocaleString()
userInfo.lastLoginAt = user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '从未登录'
// 加载用户设置
const settings = authStore.getUserSettings()
Object.assign(downloadSettings, settings)
}
}
// 保存设置
const saveSettings = () => {
try {
authStore.updateUserSettings(downloadSettings)
ElMessage.success('设置已保存')
} catch (error) {
ElMessage.error('保存设置失败')
}
}
// 清除历史记录
const clearHistory = async () => {
try {
await ElMessageBox.confirm(
'确定要清除所有下载历史记录吗?此操作不可恢复。',
'确认清除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
authStore.clearDownloadHistory()
ElMessage.success('历史记录已清除')
} catch {
// 用户取消
}
}
// 删除用户数据
const deleteUserData = async () => {
try {
await ElMessageBox.confirm(
'确定要删除当前用户的所有数据吗?包括设置、历史记录等,此操作不可恢复。',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 清除历史记录
authStore.clearDownloadHistory()
// 重置用户设置
const defaultSettings = {
maxConcurrentDownloads: 3,
fileNamePrefix: '',
enableNotifications: true
}
authStore.updateUserSettings(defaultSettings)
Object.assign(downloadSettings, defaultSettings)
ElMessage.success('用户数据已删除')
} catch {
// 用户取消
}
}
// 返回下载器
const goBack = () => {
router.push('/downloader')
}
// 组件挂载时初始化
onMounted(() => {
initializeData()
})
</script>
<style scoped>
.settings-container {
min-height: 100vh;
background: #f5f5f5;
}
.header {
background: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 600;
color: #333;
}
.main-content {
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
.settings-card {
margin-bottom: 20px;
}
.card-header {
font-size: 16px;
font-weight: 600;
color: #333;
}
.form-tip {
margin-left: 10px;
color: #666;
font-size: 12px;
}
.data-management {
display: flex;
flex-direction: column;
gap: 20px;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.data-info h4 {
margin: 0 0 5px 0;
color: #333;
font-size: 14px;
}
.data-info p {
margin: 0;
color: #666;
font-size: 12px;
}
@media (max-width: 768px) {
.main-content {
padding: 0 10px;
}
.data-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment