Update On Mon Mar 23 20:06:14 CET 2026
@@ -1307,3 +1307,4 @@ Update On Thu Mar 19 20:11:34 CET 2026
|
||||
Update On Fri Mar 20 20:03:22 CET 2026
|
||||
Update On Sat Mar 21 19:50:06 CET 2026
|
||||
Update On Sun Mar 22 19:50:55 CET 2026
|
||||
Update On Mon Mar 23 20:06:04 CET 2026
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Apple 开发者信息
|
||||
APPLE_ID=your-apple-id@example.com
|
||||
APPLE_PASSWORD=your-app-specific-password
|
||||
APPLE_TEAM_ID=your-team-id
|
||||
|
||||
# 阿里云盘配置(后端用)
|
||||
ALIYUN_APP_ID=
|
||||
ALIYUN_APP_SECRET=
|
||||
|
||||
# 阿里云盘配置(前端用)
|
||||
VITE_ALIYUN_APP_ID=
|
||||
VITE_ALIYUN_APP_SECRET=
|
||||
|
||||
# 百度网盘配置
|
||||
BAIDU_APP_ID=
|
||||
BAIDU_APP_SECRET=
|
||||
BAIDU_API_KEY=
|
||||
VITE_BAIDU_APP_ID=
|
||||
VITE_BAIDU_APP_SECRET=
|
||||
VITE_BAIDU_API_KEY=
|
||||
|
||||
# 123网盘配置
|
||||
PAN123_APP_ID=
|
||||
PAN123_APP_SECRET=
|
||||
VITE_PAN123_APP_ID=
|
||||
VITE_PAN123_APP_SECRET=
|
||||
|
||||
# 115网盘配置
|
||||
PAN115_APP_ID=
|
||||
PAN115_APP_SECRET=
|
||||
VITE_PAN115_APP_ID=
|
||||
VITE_PAN115_APP_SECRET=
|
||||
|
||||
# 可选:指定特定证书(如果有多个同类证书)
|
||||
# CSC_NAME="Developer ID Application: Your Name (TEAM_ID)"
|
||||
@@ -2,10 +2,8 @@ name: Build Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'package.json'
|
||||
tags:
|
||||
- 'v*' # 仅在推送 tag 时触发(如 v1.0.0, v2.1.3 等)
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -13,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ windows-latest, macos-latest, ubuntu-latest ]
|
||||
os: [ windows-latest, ubuntu-latest ] # 移除 macOS
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
permissions:
|
||||
@@ -97,5 +95,6 @@ jobs:
|
||||
uses: paneron/action-electron-builder@v1.8.1
|
||||
with:
|
||||
package_manager: pnpm
|
||||
release: true
|
||||
release: true # 自动创建 Release
|
||||
draft: true # 创建为草稿状态,不自动发布
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,3 +11,18 @@ release
|
||||
.idea
|
||||
|
||||
localVersion
|
||||
|
||||
# 包管理器锁文件 - 只保留 pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 环境变量和敏感配置文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
config/secrets.json
|
||||
config/keys.json
|
||||
|
||||
# 外部项目/子模块 - 不提交到主仓库
|
||||
CloudServiceKit/
|
||||
XbyVideoHub/
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
### 小白羊v3版本源码帮助
|
||||
|
||||
v3采用 ts + vue3 + vite + electron 模板开发
|
||||
|
||||
#### 1.下载源代码
|
||||
|
||||
```
|
||||
https://github.com/gaozhangmin/aliyunpan.git
|
||||
```
|
||||
|
||||
#### 2.打开代码目录,安装依赖
|
||||
|
||||
```cmd
|
||||
npm install pnpm -g
|
||||
pnpm install
|
||||
pnpm config set registry https://registry.npmmirror.com
|
||||
```
|
||||
|
||||
#### 3.环境配置
|
||||
|
||||
##### 3.1 创建环境变量文件
|
||||
|
||||
复制示例配置文件并根据需要修改:
|
||||
|
||||
```cmd
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
##### 3.2 配置网盘 API 密钥
|
||||
|
||||
编辑 `.env` 文件,配置各网盘平台的 APP_ID 和 APP_SECRET:
|
||||
|
||||
```bash
|
||||
# 阿里云盘配置
|
||||
VITE_ALIYUN_APP_ID=your_aliyun_app_id
|
||||
VITE_ALIYUN_APP_SECRET=your_aliyun_app_secret
|
||||
|
||||
# 百度网盘配置
|
||||
VITE_BAIDU_APP_ID=your_baidu_app_id
|
||||
VITE_BAIDU_APP_SECRET=your_baidu_app_secret
|
||||
VITE_BAIDU_PCS_APP_ID=your_baidu_pcs_app_id
|
||||
|
||||
# 123网盘配置
|
||||
VITE_PAN123_APP_ID=your_123pan_app_id
|
||||
VITE_PAN123_APP_SECRET=your_123pan_app_secret
|
||||
|
||||
# 115网盘配置
|
||||
VITE_PAN115_APP_ID=your_115pan_app_id
|
||||
VITE_PAN115_APP_SECRET=your_115pan_app_secret
|
||||
```
|
||||
|
||||
##### 3.3 获取网盘 API 密钥
|
||||
|
||||
**阿里云盘**:
|
||||
1. 访问 [阿里云盘开放平台](https://www.aliyundrive.com/drive/file/backup)
|
||||
2. 登录并创建应用
|
||||
3. 获取 APP_ID 和 APP_SECRET
|
||||
|
||||
**百度网盘**:
|
||||
1. 访问 [百度网盘开放平台](https://pan.baidu.com/union/doc/)
|
||||
2. 登录并创建应用
|
||||
3. 获取 API Key (APP_ID) 和 Secret Key (APP_SECRET)
|
||||
|
||||
**123云盘**:
|
||||
1. 访问 [123云盘开发者平台](https://www.123pan.com/developers)
|
||||
2. 注册开发者并创建应用
|
||||
3. 获取 ClientID 和 ClientSecret
|
||||
|
||||
**115网盘**:
|
||||
1. 联系 115 官方获取开发者权限
|
||||
2. 获取相应的 APP_ID 和 APP_SECRET
|
||||
|
||||
> **注意**:
|
||||
> - 请妥善保管你的 API 密钥,不要提交到公开仓库
|
||||
> - `.env` 文件已在 `.gitignore` 中,不会被 Git 追踪
|
||||
> - 如需自定义配置,可参考 `.env.example` 文件
|
||||
|
||||
#### 4.开发调试运行
|
||||
|
||||
```cmd
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
执行命令后会调起electron窗口,配合vscode正常开发调试即可
|
||||
|
||||
#### 5.打包
|
||||
|
||||
```cmd
|
||||
pnpm run build:electron
|
||||
```
|
||||
|
||||
#### 6.macOS 签名配置(可选)
|
||||
|
||||
如需在 macOS 上进行代码签名,需要在 `.env` 文件中配置:
|
||||
|
||||
```bash
|
||||
# Apple 开发者信息
|
||||
APPLE_ID=your-apple-id@example.com
|
||||
APPLE_PASSWORD=your-app-specific-password
|
||||
APPLE_TEAM_ID=your-team-id
|
||||
|
||||
# 可选:指定特定证书
|
||||
# CSC_NAME="Developer ID Application: Your Name (TEAM_ID)"
|
||||
```
|
||||
|
||||
然后使用签名版本打包:
|
||||
|
||||
```cmd
|
||||
pnpm run build:mac:signed
|
||||
```
|
||||
|
||||
#### 7.项目结构说明
|
||||
|
||||
```
|
||||
├── electron/ # Electron 主进程和预加载脚本
|
||||
├── src/
|
||||
│ ├── components/ # Vue 组件
|
||||
│ ├── store/ # Pinia 状态管理
|
||||
│ ├── utils/ # 工具函数
|
||||
│ ├── aliapi/ # 阿里云盘 API
|
||||
│ ├── cloudbaidu/ # 百度网盘 API
|
||||
│ ├── cloud123/ # 123云盘 API
|
||||
│ ├── cloud115/ # 115网盘 API
|
||||
│ └── views/ # 页面视图
|
||||
├── .env.example # 环境变量示例文件
|
||||
├── vite.config.ts # Vite 配置
|
||||
└── package.json # 项目依赖配置
|
||||
```
|
||||
|
||||
#### 8.常见问题
|
||||
|
||||
**Q: 启动时提示网盘 API 配置错误?**
|
||||
A: 请检查 `.env` 文件是否正确配置了相应网盘的 APP_ID 和 APP_SECRET
|
||||
|
||||
**Q: 如何添加新的网盘支持?**
|
||||
A: 参考现有网盘 API 实现,在对应目录下添加新的 API 模块
|
||||
|
||||
**Q: 打包后的应用无法正常使用网盘功能?**
|
||||
A: 确保环境变量在构建时被正确注入,检查 `vite.config.ts` 中的环境变量配置
|
||||
|
||||
#### 9.贡献代码
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
|
||||
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||
5. 创建 Pull Request
|
||||
@@ -5,7 +5,7 @@
|
||||
<br> English | <a href="README-CN.md">中文</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<em>小白羊网盘 - powered by 阿里云盘Open.</em>
|
||||
<em>小白羊网盘 - 多网盘统一管理 + 智能媒体库 + 高速下载.</em>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -49,33 +49,66 @@
|
||||
|
||||
|
||||
# 功能 [](#功能-)
|
||||
1.根据阿里云盘Open平台api开发的网盘客户端,支持win7-11,macOS,linux <br>
|
||||
|
||||
2.支持同时登录多个账号管理。 <br>
|
||||
## 🌟 多网盘支持
|
||||
1. **多平台网盘接入**:支持阿里云盘、百度网盘、123网盘、115网盘等主流网盘服务 <br>
|
||||
2. **WebDAV 连接**:支持通过 WebDAV 协议连接夸克网盘、天翼云等更多网盘服务 <br>
|
||||
3. **多账号管理**:支持同时登录和管理多个网盘账号 <br>
|
||||
|
||||
3.提供特有的文件夹树,方便快速操作。 <br>
|
||||
## 🎬 智能媒体库
|
||||
4. **TMDB 元数据刮削**:自动扫描网盘和本地文件,从 TMDB 获取电影、电视剧等媒体元数据 <br>
|
||||
5. **媒体库整理**:智能分类整理媒体文件,构建完整的个人媒体库 <br>
|
||||
6. **本地文件夹导入**:支持导入本地文件夹并识别刮削 TMDB 元数据 <br>
|
||||
|
||||
4.在线播放网盘中各种格式的高清原画视频,并支持外挂字幕、音轨和播放速度调整,播放列表。<br>
|
||||
## 🎥 强大播放功能
|
||||
7. **在线高清播放**:支持网盘中各种格式的高清原画视频播放 <br>
|
||||
8. **外挂字幕音轨**:支持外挂字幕、多音轨切换和播放速度调整 <br>
|
||||
9. **播放列表管理**:支持创建和管理播放列表 <br>
|
||||
10. **第三方播放器**:支持 MPV、IINA 等专业播放器 <br>
|
||||
|
||||
5.显示文件夹体积,支持文件夹和文件的混合排序(文件名/体积/时间)。<br>
|
||||
## 🔗 媒体服务器连接
|
||||
11. **媒体服务器支持**:支持连接 Emby、Jellyfin、Plex 等媒体服务器(部分功能待开发)<br>
|
||||
|
||||
6.可以通过远程Aria2功能将文件直接下载到远程的VPS/NAS。<br>
|
||||
## ⚡ 高速下载
|
||||
12. **Aria2c 下载**:集成高速 Aria2c 下载引擎,支持多线程下载 <br>
|
||||
13. **远程下载**:可通过远程 Aria2 功能将文件直接下载到远程 VPS/NAS <br>
|
||||
|
||||
7.支持批量重命名大量文件和多层嵌套的文件夹。<br>
|
||||
## 📁 文件管理
|
||||
14. **文件夹树视图**:提供特有的文件夹树,方便快速操作 <br>
|
||||
15. **智能排序**:显示文件夹体积,支持文件夹和文件的混合排序(文件名/体积/时间)<br>
|
||||
16. **批量操作**:支持批量重命名大量文件和多层嵌套的文件夹 <br>
|
||||
17. **快速预览**:可以快速复制文件,预览视频的雪碧图,并直接删除文件 <br>
|
||||
18. **海量文件处理**:能够管理数万文件夹和数万文件,一次性列出文件夹中的全部文件 <br>
|
||||
19. **批量传输**:支持一次性上传/下载百万级的文件/文件夹 <br>
|
||||
|
||||
8.可以快速复制文件,预览视频的雪碧图,并直接删除文件。<br>
|
||||
|
||||
9.能够管理数万文件夹和数万文件,并一次性列出文件夹中的全部文件。<br>
|
||||
|
||||
10.支持一次性上传/下载百万级的文件/文件夹。<br>
|
||||
## 🖥️ 跨平台支持
|
||||
20. **全平台兼容**:支持 Windows 7-11、macOS、Linux 等操作系统 <br>
|
||||
|
||||
<a href="#readme">
|
||||
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
|
||||
</a>
|
||||
|
||||
# 界面 [](#界面-)
|
||||
<img src="https://github.com/gaozhangmin/staticResource/blob/master/images/main_window.png" width="270"><img src="https://github.com/gaozhangmin/staticResource/blob/master/images/download_page.png" width="270"><img src="https://github.com/gaozhangmin/staticResource/blob/master/images/movie_page.png" width="270">
|
||||
<img src="https://github.com/gaozhangmin/staticResource/blob/master/images/plugin_page.png" width="270"><img src="https://github.com/gaozhangmin/staticResource/blob/master/images/settings_page.png" width="270"><img src="https://github.com/gaozhangmin/staticResource/blob/master/images/share_page.png" width="270">
|
||||
|
||||
## 📂 文件管理界面
|
||||
<img src="images/file-manager.png" width="400"> <img src="images/folder-tree.png" width="400">
|
||||
|
||||
*文件管理主界面 & 文件夹树视图*
|
||||
|
||||
## 👤 多账号登录
|
||||
<img src="images/multi-account.png" width="400"> <img src="images/login-qr.png" width="400">
|
||||
|
||||
*多网盘账号管理 & 二维码登录*
|
||||
|
||||
## 🎬 智能媒体库
|
||||
<img src="images/media-library.png" width="400"> <img src="images/movie-grid.png" width="400">
|
||||
|
||||
*媒体库海报墙 & 网格视图*
|
||||
|
||||
## 🎥 媒体详情
|
||||
<img src="images/movie-list.png" width="400"> <img src="images/movie-details.png" width="400">
|
||||
|
||||
*媒体列表视图 & 电影详情页面*
|
||||
<a href="#readme">
|
||||
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="#" align="right">
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Electron 应用必需的代码执行权限 -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- 网络访问权限 - 网盘客户端核心功能 -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
<!-- 文件系统访问权限 - 上传下载文件必需 -->
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,217 @@
|
||||
# 环境变量配置说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
为了保护敏感信息(如各网盘的 App ID 和 App Secret),本项目使用环境变量文件来统一管理所有的敏感配置。项目支持 Node.js 环境变量和 Vite 前端环境变量两套配置。
|
||||
|
||||
## 🔧 配置步骤
|
||||
|
||||
### 1. 后端配置(Node.js)
|
||||
```bash
|
||||
cp .env.template .env
|
||||
```
|
||||
|
||||
### 2. 前端配置(Vite)
|
||||
```bash
|
||||
cp .env.development.template .env.development
|
||||
cp .env.production.template .env.production
|
||||
```
|
||||
|
||||
### 3. 填入真实的配置信息
|
||||
编辑对应的环境变量文件,将占位符替换为真实的密钥:
|
||||
|
||||
**后端 (.env):**
|
||||
```bash
|
||||
# 阿里云盘配置
|
||||
ALIYUN_APP_ID=your_real_app_id
|
||||
ALIYUN_APP_SECRET=your_real_app_secret
|
||||
```
|
||||
|
||||
**前端 (.env.development):**
|
||||
```bash
|
||||
# 阿里云盘配置
|
||||
VITE_ALIYUN_APP_ID=your_real_app_id
|
||||
VITE_ALIYUN_APP_SECRET=your_real_app_secret
|
||||
```
|
||||
|
||||
## 🔒 安全机制
|
||||
|
||||
### 自动清理
|
||||
- 每次 `git commit` 前,系统会自动将所有环境变量文件中的密钥值清空
|
||||
- 支持多个环境文件:`.env`, `.env.development`, `.env.production`
|
||||
- 只保留配置键名,确保不会意外提交敏感信息到 GitHub
|
||||
|
||||
### 文件保护
|
||||
- 所有包含真实密钥的环境变量文件都在 `.gitignore` 中
|
||||
- 只有模板文件会被提交到版本控制
|
||||
- 前端和后端配置分离管理
|
||||
|
||||
## 📝 在代码中使用配置
|
||||
|
||||
### 前端 Vue 组件中使用
|
||||
```typescript
|
||||
import appConfig from './src/utils/appconfig';
|
||||
|
||||
// 获取阿里云盘配置
|
||||
const { appId, appSecret } = appConfig.getAliyunConfig();
|
||||
|
||||
// 获取其他网盘配置
|
||||
const baiduConfig = appConfig.getBaiduConfig();
|
||||
const tmdbConfig = appConfig.getTmdbConfig();
|
||||
|
||||
// 检查配置完整性
|
||||
if (!appConfig.validateConfig()) {
|
||||
console.error('配置不完整,请检查环境变量文件');
|
||||
}
|
||||
```
|
||||
|
||||
### 后端 Node.js 中使用
|
||||
```javascript
|
||||
const config = require('./src/utils/config');
|
||||
|
||||
// 获取阿里云盘配置
|
||||
const aliyunConfig = config.getAliyunConfig();
|
||||
|
||||
// 直接获取环境变量
|
||||
const aliyunAppId = config.get('ALIYUN_APP_ID');
|
||||
```
|
||||
|
||||
## 🛠 维护和更新
|
||||
|
||||
### 添加新的配置项
|
||||
1. 在所有模板文件中添加新的配置键
|
||||
2. 在对应的实际环境文件中添加真实值
|
||||
3. 在配置管理器中添加对应的获取方法
|
||||
4. 确保清理脚本能识别新的配置项
|
||||
|
||||
### 手动清理敏感信息
|
||||
```bash
|
||||
node scripts/clean-env.js
|
||||
```
|
||||
|
||||
# 环境变量配置说明
|
||||
|
||||
## 📋 概述
|
||||
|
||||
为了保护敏感信息(如各网盘的 App ID 和 App Secret),本项目使用统一的环境变量文件来管理所有的敏感配置。不区分开发和生产环境,简化配置管理。
|
||||
|
||||
## 🔧 配置步骤
|
||||
|
||||
### 1. 复制模板文件
|
||||
```bash
|
||||
cp .env.template .env
|
||||
```
|
||||
|
||||
### 2. 填入真实的配置信息
|
||||
编辑 `.env` 文件,将占位符替换为真实的密钥:
|
||||
|
||||
```bash
|
||||
# 阿里云盘配置(后端用)
|
||||
ALIYUN_APP_ID=your_real_app_id
|
||||
ALIYUN_APP_SECRET=your_real_app_secret
|
||||
|
||||
# 阿里云盘配置(前端用)
|
||||
VITE_ALIYUN_APP_ID=your_real_app_id
|
||||
VITE_ALIYUN_APP_SECRET=your_real_app_secret
|
||||
```
|
||||
|
||||
**注意**:前端可访问的环境变量必须以 `VITE_` 开头。
|
||||
|
||||
## 🔒 安全机制
|
||||
|
||||
### 自动清理
|
||||
- 每次 `git commit` 前,系统会自动将 `.env` 文件中的所有密钥值清空
|
||||
- 只保留配置键名,确保不会意外提交敏感信息到 GitHub
|
||||
- 支持前端和后端的环境变量(VITE_ 前缀和普通前缀)
|
||||
|
||||
### 文件保护
|
||||
- `.env` 文件已添加到 `.gitignore`,不会被提交到版本控制
|
||||
- `.env.template` 作为模板文件,只包含键名,可以安全提交
|
||||
- 统一配置,简化管理
|
||||
|
||||
## 📝 在代码中使用配置
|
||||
|
||||
### 前端 Vue 组件中使用
|
||||
```typescript
|
||||
import appConfig from './src/utils/appconfig';
|
||||
|
||||
// 获取阿里云盘配置
|
||||
const { appId, appSecret } = appConfig.getAliyunConfig();
|
||||
|
||||
// 获取其他网盘配置
|
||||
const baiduConfig = appConfig.getBaiduConfig();
|
||||
const tmdbConfig = appConfig.getTmdbConfig();
|
||||
|
||||
// 检查配置完整性
|
||||
if (!appConfig.validateConfig()) {
|
||||
console.error('配置不完整,请检查环境变量文件');
|
||||
}
|
||||
```
|
||||
|
||||
### 后端 Node.js 中使用
|
||||
```javascript
|
||||
const config = require('./src/utils/config');
|
||||
|
||||
// 获取阿里云盘配置
|
||||
const aliyunConfig = config.getAliyunConfig();
|
||||
|
||||
// 直接获取环境变量
|
||||
const aliyunAppId = config.get('ALIYUN_APP_ID');
|
||||
```
|
||||
|
||||
## 🛠 维护和更新
|
||||
|
||||
### 添加新的配置项
|
||||
1. 在 `.env.template` 中添加新的配置键(后端和前端版本)
|
||||
2. 在 `.env` 中添加相应的真实值
|
||||
3. 在配置管理器中添加对应的获取方法
|
||||
|
||||
### 手动清理敏感信息
|
||||
```bash
|
||||
node scripts/clean-env.js
|
||||
```
|
||||
|
||||
## 🏗 项目结构
|
||||
|
||||
```
|
||||
├── .env # 统一环境变量文件(开发+生产)
|
||||
├── .env.template # 环境变量模板文件
|
||||
├── src/utils/config.js # 后端配置管理器
|
||||
├── src/utils/appconfig.ts # 前端配置管理器
|
||||
└── scripts/clean-env.js # 环境变量清理脚本
|
||||
```
|
||||
|
||||
## ⚠️ 重要提醒
|
||||
|
||||
1. **环境变量前缀**:前端可访问的变量必须以 `VITE_` 开头
|
||||
2. **双重配置**:对于需要前后端都访问的配置,需要配置两个版本(VITE_ 和普通)
|
||||
3. **备份重要**:在本地保留包含真实密钥的 `.env` 文件备份
|
||||
4. **统一管理**:不区分开发和生产环境,使用同一套配置
|
||||
5. **定期检查**:确保 `.gitignore` 正确配置,避免意外提交敏感信息
|
||||
|
||||
## 📱 支持的网盘服务
|
||||
|
||||
当前配置支持以下服务(每个服务都有后端和前端两个版本):
|
||||
|
||||
- ✅ 阿里云盘 (ALIYUN_APP_ID / VITE_ALIYUN_APP_ID, ALIYUN_APP_SECRET / VITE_ALIYUN_APP_SECRET)
|
||||
- ✅ 百度网盘 (BAIDU_*, VITE_BAIDU_*)
|
||||
- ✅ 123网盘 (PAN123_*, VITE_PAN123_*)
|
||||
- ✅ 115网盘 (PAN115_*, VITE_PAN115_*)
|
||||
- ✅ 夸克网盘 (QUARK_*, VITE_QUARK_*)
|
||||
- ✅ 天翼云盘 (TIANYI_*, VITE_TIANYI_*)
|
||||
- ✅ TMDB API (TMDB_API_KEY / VITE_TMDB_API_KEY)
|
||||
- ✅ 其他配置 (ARIA2_SECRET, DATABASE_URL, JWT_SECRET)
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
| 运行方式 | 使用的环境文件 | 说明 |
|
||||
|----------|----------------|------|
|
||||
| `pnpm dev` | `.env` | 开发调试 |
|
||||
| `pnpm build` | `.env` | 构建网页版 |
|
||||
| `pnpm build:electron` | `.env` | 打包桌面应用 |
|
||||
|
||||
**简化优势**:
|
||||
- ✅ 只需要维护一个 `.env` 文件
|
||||
- ✅ 配置统一,减少出错
|
||||
- ✅ 适合桌面应用的使用场景
|
||||
- ✅ 自动清理机制保证安全
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"appId": "com.alixby.app",
|
||||
"productName": "阿里云盘小白羊",
|
||||
"productName": "小白羊 BoxPlayer",
|
||||
"protocols": [
|
||||
{
|
||||
"name": "xbyboxplayer OAuth",
|
||||
"schemes": ["xbyboxplayer-oauth"]
|
||||
}
|
||||
],
|
||||
"copyright": "copyright ©2023 gaozhangmin",
|
||||
"asar": true,
|
||||
"compression": "maximum",
|
||||
@@ -16,7 +22,10 @@
|
||||
"artifactName": "alixby-${version}-mac-${arch}.${ext}",
|
||||
"darkModeSupport": true,
|
||||
"hardenedRuntime": true,
|
||||
"gatekeeperAssess": false,
|
||||
"category": "public.app-category.utilities",
|
||||
"entitlements": "./build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "./build/entitlements.mac.plist",
|
||||
"extraResources": [
|
||||
{ "from": "./static/images/icon.icns", "to": "./images/icon.icns"},
|
||||
{ "from": "./static/images/icon_30x30.png", "to": "./images/icon_30x30.png"},
|
||||
@@ -25,7 +34,8 @@
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": [ "x64", "arm64" ] },
|
||||
{ "target": "zip", "arch": [ "x64", "arm64" ] }
|
||||
]
|
||||
],
|
||||
"notarize": false
|
||||
},
|
||||
"linux": {
|
||||
"icon": "./static/images/icon_256x256.png",
|
||||
|
||||
@@ -127,7 +127,7 @@ export function createMainWindow() {
|
||||
AppWindow.mainWindow.on('ready-to-show', function() {
|
||||
AppWindow.mainWindow!.webContents.send('setPage', { page: 'PageMain' })
|
||||
AppWindow.mainWindow!.webContents.send('setTheme', { dark: nativeTheme.shouldUseDarkColors })
|
||||
AppWindow.mainWindow!.setTitle('阿里云盘小白羊')
|
||||
AppWindow.mainWindow!.setTitle('小白羊 BoxPlayer')
|
||||
if (is.windows() && process.argv && process.argv.join(' ').indexOf('--openAsHidden') < 0) {
|
||||
AppWindow.mainWindow!.show()
|
||||
} else if (is.macOS() && !app.getLoginItemSettings().wasOpenedAsHidden) {
|
||||
@@ -179,7 +179,7 @@ export function createTray() {
|
||||
const icon = getStaticPath('icon_256x256.ico')
|
||||
AppWindow.appTray = new Tray(icon)
|
||||
const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
|
||||
AppWindow.appTray.setToolTip('阿里云盘小白羊')
|
||||
AppWindow.appTray.setToolTip('小白羊 BoxPlayer')
|
||||
AppWindow.appTray.setContextMenu(contextMenu)
|
||||
AppWindow.appTray.on('click', () => {
|
||||
if (AppWindow.mainWindow && AppWindow.mainWindow.isDestroyed() == false) {
|
||||
@@ -367,7 +367,7 @@ function createUpload() {
|
||||
AppWindow.uploadWindow.on('ready-to-show', function() {
|
||||
creatUploadPort()
|
||||
AppWindow.uploadWindow!.webContents.send('setPage', { page: 'PageWorker', data: { type: 'upload' } })
|
||||
AppWindow.uploadWindow!.setTitle('阿里云盘小白羊上传进程')
|
||||
AppWindow.uploadWindow!.setTitle('小白羊 BoxPlayer上传进程')
|
||||
})
|
||||
|
||||
AppWindow.uploadWindow.webContents.on('render-process-gone', function(event, details) {
|
||||
@@ -391,7 +391,7 @@ function createDownload() {
|
||||
AppWindow.downloadWindow.on('ready-to-show', function() {
|
||||
creatDownloadPort()
|
||||
AppWindow.downloadWindow!.webContents.send('setPage', { page: 'PageWorker', data: { type: 'download' } })
|
||||
AppWindow.downloadWindow!.setTitle('阿里云盘小白羊下载进程')
|
||||
AppWindow.downloadWindow!.setTitle('小白羊 BoxPlayer下载进程')
|
||||
})
|
||||
|
||||
AppWindow.downloadWindow.webContents.on('render-process-gone', function(event, details) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getStaticPath } from './utils/mainfile'
|
||||
import launch from './launch'
|
||||
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: '阿里云盘小白羊',
|
||||
applicationName: '小白羊 BoxPlayer',
|
||||
copyright: 'copyright ©2023 gaozhangmin',
|
||||
website: 'https://github.com/gaozhangmin/aliyunpan',
|
||||
iconPath: getStaticPath('icon_64x64.png'),
|
||||
|
||||
@@ -13,9 +13,13 @@ type UserToken = {
|
||||
access_token: string;
|
||||
open_api_access_token: string;
|
||||
user_id: string;
|
||||
tokenfrom?: string;
|
||||
refresh: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_DOWN_AGENT =
|
||||
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) aDrive/4.12.0 Chrome/108.0.5359.215 Electron/22.3.24 Safari/537.36'
|
||||
|
||||
export default class launch extends EventEmitter {
|
||||
private userToken: UserToken = {
|
||||
access_token: '',
|
||||
@@ -23,6 +27,7 @@ export default class launch extends EventEmitter {
|
||||
user_id: '',
|
||||
refresh: false
|
||||
}
|
||||
private pendingOAuthUrl: string | null = null
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -39,7 +44,14 @@ export default class launch extends EventEmitter {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
if (commandLine && commandLine.join(' ').indexOf('exit') >= 0) {
|
||||
this.hasExitArgv(commandLine)
|
||||
} else if (AppWindow.mainWindow && AppWindow.mainWindow.isDestroyed() == false) {
|
||||
return
|
||||
}
|
||||
const oauthUrl = this.extractOAuthUrl(commandLine)
|
||||
if (oauthUrl) {
|
||||
this.dispatchOAuthUrl(oauthUrl)
|
||||
return
|
||||
}
|
||||
if (AppWindow.mainWindow && AppWindow.mainWindow.isDestroyed() == false) {
|
||||
if (AppWindow.mainWindow.isMinimized()) {
|
||||
AppWindow.mainWindow.restore()
|
||||
}
|
||||
@@ -109,12 +121,14 @@ export default class launch extends EventEmitter {
|
||||
this.handleAppActivate()
|
||||
this.handleAppWillQuit()
|
||||
this.handleAppWindowAllClosed()
|
||||
this.handleProtocolCallback()
|
||||
}
|
||||
|
||||
handleAppReady() {
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
this.registerProtocol()
|
||||
try {
|
||||
const localVersion = getResourcesPath('localVersion')
|
||||
if (localVersion && existsSync(localVersion)) {
|
||||
@@ -129,6 +143,8 @@ export default class launch extends EventEmitter {
|
||||
}
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||
const shouldGieeReferer = details.url.indexOf('gitee.com') > 0
|
||||
const shouldBaidu = /baidu|baidupcs|bdstatic|bcebos/i.test(details.url)
|
||||
const should115 = /(^https?:\/\/[^/]*115\.com\/)|(^https?:\/\/[^/]*anxia\.com\/)/i.test(details.url)
|
||||
const shouldBiliBili = details.url.indexOf('bilibili.com') > 0
|
||||
const shouldQQTv = details.url.indexOf('v.qq.com') > 0 || details.url.indexOf('video.qq.com') > 0
|
||||
const shouldAliPanOrigin = details.url.indexOf('.aliyundrive.com') > 0 || details.url.indexOf('.alipan.com') > 0
|
||||
@@ -141,13 +157,16 @@ export default class launch extends EventEmitter {
|
||||
requestHeaders: {
|
||||
...details.requestHeaders,
|
||||
...(shouldGieeReferer && {
|
||||
Referer: 'https://gitee.com/'
|
||||
Referer: 'https://gitee.com/',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
...(shouldAliPanOrigin && {
|
||||
Origin: 'https://www.aliyundrive.com'
|
||||
Origin: 'https://www.aliyundrive.com',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
...(shouldAliReferer && {
|
||||
Referer: 'https://www.aliyundrive.com/'
|
||||
Referer: 'https://www.aliyundrive.com/',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
...(shouldBiliBili && {
|
||||
Referer: 'https://www.bilibili.com/',
|
||||
@@ -159,13 +178,25 @@ export default class launch extends EventEmitter {
|
||||
Origin: 'https://m.v.qq.com',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 13; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
...(shouldBaidu && {
|
||||
Referer: 'https://pan.baidu.com/',
|
||||
Origin: 'https://pan.baidu.com',
|
||||
'user-agent': 'pan.baidu.com'
|
||||
}),
|
||||
...(should115 && {
|
||||
...(this.userToken.tokenfrom === '115' && this.userToken.access_token
|
||||
? { Authorization: `Bearer ${this.userToken.access_token}` }
|
||||
: {}),
|
||||
'user-agent': DEFAULT_DOWN_AGENT
|
||||
}),
|
||||
...(shouldToken && {
|
||||
Authorization: this.userToken.access_token
|
||||
Authorization: this.userToken.access_token,
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
...(shouldOpenApiToken && {
|
||||
Authorization: this.userToken.open_api_access_token
|
||||
Authorization: this.userToken.open_api_access_token,
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0'
|
||||
}),
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) aDrive/4.12.0 Chrome/108.0.5359.215 Electron/22.3.24 Safari/537.36',
|
||||
'X-Canary': 'client=windows,app=adrive,version=v4.12.0',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9'
|
||||
}
|
||||
@@ -174,6 +205,10 @@ export default class launch extends EventEmitter {
|
||||
session.defaultSession.loadExtension(getStaticPath('crx'), { allowFileAccess: true }).then(() => {
|
||||
createMainWindow()
|
||||
createTray()
|
||||
if (this.pendingOAuthUrl) {
|
||||
this.dispatchOAuthUrl(this.pendingOAuthUrl)
|
||||
this.pendingOAuthUrl = null
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err: any) => {
|
||||
@@ -227,4 +262,37 @@ export default class launch extends EventEmitter {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private registerProtocol() {
|
||||
const protocol = 'xbyboxplayer-oauth'
|
||||
if (is.windows() && process.defaultApp && process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(protocol, process.execPath, [process.argv[1]])
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(protocol)
|
||||
}
|
||||
}
|
||||
|
||||
private handleProtocolCallback() {
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
if (url) this.dispatchOAuthUrl(url)
|
||||
})
|
||||
}
|
||||
|
||||
private extractOAuthUrl(commandLine?: string[]) {
|
||||
if (!commandLine) return ''
|
||||
const prefix = 'xbyboxplayer-oauth://'
|
||||
return commandLine.find(arg => arg.startsWith(prefix)) || ''
|
||||
}
|
||||
|
||||
private dispatchOAuthUrl(url: string) {
|
||||
if (!url) return
|
||||
if (AppWindow.mainWindow && AppWindow.mainWindow.isDestroyed() === false) {
|
||||
AppWindow.mainWindow.webContents.send('cloud123-oauth-callback', url)
|
||||
AppWindow.mainWindow.show()
|
||||
AppWindow.mainWindow.focus()
|
||||
} else {
|
||||
this.pendingOAuthUrl = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 943 KiB |
|
After Width: | Height: | Size: 965 KiB |
|
After Width: | Height: | Size: 385 KiB |
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>阿里云盘小白羊</title>
|
||||
<title>小白羊 BoxPlayer</title>
|
||||
<meta name="data-spm" content="aliyundrive" />
|
||||
<link rel="icon" href='/favicon.ico' />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "alixby",
|
||||
"description": "阿里云盘小白羊",
|
||||
"version": "3.24.41217",
|
||||
"description": "小白羊 BoxPlayer",
|
||||
"version": "4.0.0-beta",
|
||||
"main": "dist/electron/main/index.js",
|
||||
"author": {
|
||||
"name": "gaozhangmin",
|
||||
@@ -13,8 +13,10 @@
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:version": "node version.mjs",
|
||||
"build:electron": "pnpm run build && electron-builder",
|
||||
"build:test": "pnpm run build && electron-builder --dir"
|
||||
"build:electron": "npm run build && electron-builder",
|
||||
"build:test": "npm run build && electron-builder --dir",
|
||||
"build:mac": "npm run build && electron-builder --mac",
|
||||
"build:mac:signed": "npm run build && CSC_IDENTITY_AUTO_DISCOVERY=true electron-builder --mac"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -22,6 +24,7 @@
|
||||
"devDependencies": {
|
||||
"@arco-design/web-vue": "2.55.1",
|
||||
"@arco-themes/vue-gi-demo": "^0.0.48",
|
||||
"@electron/notarize": "^3.1.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/fast-levenshtein": "^0.0.4",
|
||||
"@types/howler": "^2.2.11",
|
||||
@@ -39,7 +42,8 @@
|
||||
"@vue/runtime-dom": "3.4.21",
|
||||
"ant-design-vue": "4.1.2",
|
||||
"aria2-lib": "1.0.1",
|
||||
"artplayer": "^5.1.1",
|
||||
"artplayer": "^5.4.0",
|
||||
"artplayer-plugin-danmuku": "^5.3.0",
|
||||
"axios": "^1.6.8",
|
||||
"chinese-simple2traditional": "^2.1.0",
|
||||
"consola": "^3.2.3",
|
||||
@@ -85,6 +89,7 @@
|
||||
"vite-plugin-electron-renderer": "0.14.5",
|
||||
"vue": "3.4.21",
|
||||
"vue-tsc": "^2.0.10",
|
||||
"webdav": "5.9.0",
|
||||
"webdav-server": "^2.6.2",
|
||||
"whacko": "^0.19.1"
|
||||
},
|
||||
@@ -103,4 +108,4 @@
|
||||
"vue3",
|
||||
"vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>阿里云盘小白羊</title>
|
||||
<title>小白羊 BoxPlayer</title>
|
||||
<meta name="data-spm" content="aliyundrive" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>阿里云盘小白羊</title>
|
||||
<title>小白羊 BoxPlayer</title>
|
||||
<meta name="data-spm" content="aliyundrive" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 965 KiB |
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 943 KiB |
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
/**
|
||||
* 检测代码中是否还有硬编码的敏感信息
|
||||
*
|
||||
* 注意:真实项目中应该将具体的敏感信息模式配置在环境变量或配置文件中,
|
||||
* 不应该直接写在代码里。这里只保留示例模式。
|
||||
*/
|
||||
|
||||
// 常见的敏感信息模式(示例模式,不包含真实密钥)
|
||||
const SENSITIVE_PATTERNS = [
|
||||
// 长度超过20的字母数字组合(可能是密钥)
|
||||
/['"`]([a-zA-Z0-9]{25,})['"`]/g,
|
||||
// 示例模式,替换真实密钥检查
|
||||
/EXAMPLE_KEY_PATTERN/g,
|
||||
/ANOTHER_EXAMPLE_PATTERN/g,
|
||||
];
|
||||
|
||||
// 需要检查的文件模式
|
||||
const FILE_PATTERNS = [
|
||||
'src/**/*.js',
|
||||
'src/**/*.ts',
|
||||
'src/**/*.vue',
|
||||
];
|
||||
|
||||
function checkHardcodedSecrets() {
|
||||
console.log('🔍 检查代码中的硬编码敏感信息...\n');
|
||||
|
||||
let foundSecrets = [];
|
||||
|
||||
try {
|
||||
// 使用 grep 搜索敏感信息
|
||||
const grepPatterns = [
|
||||
'EXAMPLE_KEY_PATTERN',
|
||||
'ANOTHER_EXAMPLE_PATTERN'
|
||||
// 真实的搜索模式应该在这里配置,但不包含在代码中
|
||||
];
|
||||
|
||||
grepPatterns.forEach(pattern => {
|
||||
try {
|
||||
const result = execSync(`grep -r "${pattern}" src/ --include="*.ts" --include="*.js" --include="*.vue" || true`,
|
||||
{ encoding: 'utf8', cwd: process.cwd() });
|
||||
|
||||
if (result.trim()) {
|
||||
foundSecrets.push({
|
||||
pattern: pattern,
|
||||
matches: result.trim().split('\n').filter(line => line.trim())
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略 grep 没找到匹配的情况
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('⚠️ 使用文件扫描模式(grep 不可用)');
|
||||
// 如果 grep 不可用,回退到文件扫描
|
||||
// 这里可以添加备用的文件扫描逻辑
|
||||
}
|
||||
|
||||
if (foundSecrets.length > 0) {
|
||||
console.log('❌ 发现硬编码的敏感信息:');
|
||||
foundSecrets.forEach(({ pattern, matches }) => {
|
||||
console.log(`\n🚨 模式: ${pattern}`);
|
||||
matches.forEach(match => {
|
||||
console.log(` ${match}`);
|
||||
});
|
||||
});
|
||||
console.log('\n⚠️ 请将这些硬编码的配置移除,确保只从环境变量读取!');
|
||||
return false;
|
||||
} else {
|
||||
console.log('✅ 未发现硬编码的敏感信息');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('🔒 Git Pre-commit: 硬编码敏感信息检查\n');
|
||||
|
||||
const isClean = checkHardcodedSecrets();
|
||||
|
||||
if (!isClean) {
|
||||
console.log('\n❌ 检查失败!请修复硬编码问题后再提交。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n✅ 硬编码检查通过!');
|
||||
}
|
||||
|
||||
// 如果是直接运行此脚本
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { checkHardcodedSecrets };
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Git 提交前清理敏感信息脚本
|
||||
* 只清理模板文件(.env.template, .env.example)中的敏感信息
|
||||
* .env 文件不会被清理,因为它在 .gitignore 中被忽略,不会提交到 GitHub
|
||||
*/
|
||||
|
||||
const ENV_FILES = []; // 不清理 .env 文件,因为它不会提交到 GitHub
|
||||
const ENV_TEMPLATE_FILES = ['.env.example']; // 只保留 .env.example,不创建 .env.template
|
||||
|
||||
function cleanEnvFiles() {
|
||||
console.log('🔒 开始清理环境变量文件...\n');
|
||||
|
||||
let totalClearedKeys = [];
|
||||
let totalModifiedFiles = [];
|
||||
|
||||
ENV_FILES.forEach(envFile => {
|
||||
if (!fs.existsSync(envFile)) {
|
||||
console.log(`⚠️ ${envFile} 文件不存在,跳过清理`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let content = fs.readFileSync(envFile, 'utf8');
|
||||
let modified = false;
|
||||
let clearedKeys = [];
|
||||
|
||||
// 使用正则表达式匹配所有的 KEY=value 格式
|
||||
const keyValuePattern = /^([A-Z_][A-Z0-9_]*)\s*=\s*(.+)$/gm;
|
||||
|
||||
content = content.replace(keyValuePattern, (match, key, value) => {
|
||||
// 如果值不为空且不是占位符,则清空它
|
||||
if (value.trim() &&
|
||||
!value.includes('your_actual_') &&
|
||||
!value.includes('YOUR_') &&
|
||||
value !== '') {
|
||||
clearedKeys.push(key);
|
||||
modified = true;
|
||||
return `${key}=`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(envFile, content, 'utf8');
|
||||
totalModifiedFiles.push(envFile);
|
||||
totalClearedKeys.push(...clearedKeys);
|
||||
console.log(`✅ 已清理 ${envFile} 中的 ${clearedKeys.length} 个环境变量`);
|
||||
} else {
|
||||
console.log(`✅ ${envFile} 已是安全状态,无需清理`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 清理 ${envFile} 时出错: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
if (totalModifiedFiles.length > 0) {
|
||||
console.log(`\n✅ 总共清理了 ${totalModifiedFiles.length} 个文件中的 ${totalClearedKeys.length} 个环境变量`);
|
||||
const uniqueKeys = [...new Set(totalClearedKeys)];
|
||||
console.log(`🔍 清理的变量类型: ${uniqueKeys.join(', ')}`);
|
||||
} else {
|
||||
console.log('\n✅ 所有环境变量文件已是安全状态,无需清理');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTemplateExists() {
|
||||
ENV_TEMPLATE_FILES.forEach(templateFile => {
|
||||
if (!fs.existsSync(templateFile)) {
|
||||
console.log(`📝 创建环境变量模板文件: ${templateFile}`);
|
||||
|
||||
const templateContent = `# Apple 开发者信息
|
||||
APPLE_ID=your-apple-id@example.com
|
||||
APPLE_PASSWORD=your-app-specific-password
|
||||
APPLE_TEAM_ID=your-team-id
|
||||
|
||||
# 阿里云盘配置(后端用)
|
||||
ALIYUN_APP_ID=
|
||||
ALIYUN_APP_SECRET=
|
||||
|
||||
# 阿里云盘配置(前端用)
|
||||
VITE_ALIYUN_APP_ID=
|
||||
VITE_ALIYUN_APP_SECRET=
|
||||
|
||||
# 百度网盘配置
|
||||
BAIDU_APP_ID=
|
||||
BAIDU_APP_SECRET=
|
||||
BAIDU_API_KEY=
|
||||
VITE_BAIDU_APP_ID=
|
||||
VITE_BAIDU_APP_SECRET=
|
||||
VITE_BAIDU_API_KEY=
|
||||
|
||||
# 123网盘配置
|
||||
PAN123_APP_ID=
|
||||
PAN123_APP_SECRET=
|
||||
VITE_PAN123_APP_ID=
|
||||
VITE_PAN123_APP_SECRET=
|
||||
|
||||
# 115网盘配置
|
||||
PAN115_APP_ID=
|
||||
PAN115_APP_SECRET=
|
||||
VITE_PAN115_APP_ID=
|
||||
VITE_PAN115_APP_SECRET=
|
||||
|
||||
# 可选:指定特定证书(如果有多个同类证书)
|
||||
# CSC_NAME="Developer ID Application: Your Name (TEAM_ID)"
|
||||
`;
|
||||
|
||||
fs.writeFileSync(templateFile, templateContent, 'utf8');
|
||||
console.log(`✅ ${templateFile} 创建完成`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('🚀 Git Pre-commit: 环境变量安全清理\n');
|
||||
|
||||
ensureTemplateExists();
|
||||
cleanEnvFiles();
|
||||
|
||||
console.log('\n🔒 环境变量清理完成!');
|
||||
console.log('\n💡 提示:');
|
||||
console.log(' - 所有环境变量文件中的敏感信息已被清空');
|
||||
console.log(' - 请在本地保留包含真实密钥的环境变量文件');
|
||||
console.log(' - 开发时使用模板文件作为参考');
|
||||
}
|
||||
|
||||
// 如果是直接运行此脚本
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { cleanEnvFiles, ensureTemplateExists };
|
||||
@@ -0,0 +1,50 @@
|
||||
console.log('🛠️ MediaLibrary 滚动功能修复测试')
|
||||
console.log('=====================================')
|
||||
|
||||
console.log('📋 问题诊断:')
|
||||
console.log('- 用户反映网格和列表模式都无法滚动浏览')
|
||||
console.log('- 这通常是由于CSS布局问题导致的')
|
||||
|
||||
console.log('\n🔧 修复方案:')
|
||||
console.log('1. 确保 .media-library 使用 flex 布局')
|
||||
console.log(' - height: 100%')
|
||||
console.log(' - display: flex')
|
||||
console.log(' - flex-direction: column')
|
||||
|
||||
console.log('\n2. 修复 .library-content 样式')
|
||||
console.log(' - flex: 1 (占据剩余空间)')
|
||||
console.log(' - overflow-y: auto (允许垂直滚动)')
|
||||
console.log(' - padding: 0 (避免影响滚动计算)')
|
||||
|
||||
console.log('\n3. 调整 .media-container 样式')
|
||||
console.log(' - width: 100%')
|
||||
console.log(' - height: 100%')
|
||||
console.log(' - overflow-y: auto (内容溢出时滚动)')
|
||||
|
||||
console.log('\n4. 为内容区域添加内边距')
|
||||
console.log(' - .media-grid: padding: 16px')
|
||||
console.log(' - .media-list: padding: 16px')
|
||||
|
||||
console.log('\n📐 布局结构:')
|
||||
console.log('```')
|
||||
console.log('.media-library (height: 100%, flex column)')
|
||||
console.log('├── .library-header (固定高度)')
|
||||
console.log('└── .library-content (flex: 1, overflow-y: auto)')
|
||||
console.log(' └── .media-container (height: 100%, overflow-y: auto)')
|
||||
console.log(' ├── .media-grid (grid layout + padding)')
|
||||
console.log(' └── .media-list (flex column + padding)')
|
||||
console.log('```')
|
||||
|
||||
console.log('\n✅ 预期效果:')
|
||||
console.log('- 当媒体项目超出容器高度时,自动出现垂直滚动条')
|
||||
console.log('- 网格视图:多行卡片可以上下滚动')
|
||||
console.log('- 列表视图:长列表可以上下滚动')
|
||||
console.log('- 滚动条只出现在内容区域,头部固定不动')
|
||||
|
||||
console.log('\n🎯 关键修复点:')
|
||||
console.log('- 添加了缺失的 .library-content 样式')
|
||||
console.log('- 给 .media-container 添加了 height: 100% 和 overflow-y: auto')
|
||||
console.log('- 将内边距移到具体的内容区域 (.media-grid, .media-list)')
|
||||
console.log('- 确保了正确的 flex 布局层次结构')
|
||||
|
||||
console.log('\n🚀 修复完成! 现在应该可以正常滚动浏览媒体内容了。')
|
||||
@@ -7,6 +7,7 @@ import AliUser from './user'
|
||||
import message from '../utils/message'
|
||||
import DebugLog from '../utils/debuglog'
|
||||
import { v4 } from 'uuid'
|
||||
import { isBaiduUser, isCloud123User, isDrive115User } from './utils'
|
||||
|
||||
export interface IUrlRespData {
|
||||
code: number
|
||||
@@ -99,6 +100,13 @@ export default class AliHttp {
|
||||
// 自动刷新Token
|
||||
if (data.code == 'AccessTokenInvalid' || data.code == 'AccessTokenExpired') {
|
||||
if (token) {
|
||||
if (isDrive115User(token) || isBaiduUser(token) || isCloud123User(token)) {
|
||||
return {
|
||||
code: error.response.status || 403,
|
||||
header: JSON.stringify(error.response.headers || {}),
|
||||
body: error.response.data || 'NetError 当前网盘接口请求错误'
|
||||
} as IUrlRespData
|
||||
}
|
||||
const isOpenApi = config.url.includes('adrive/v1.0') || config.url.includes('adrive/v1.1')
|
||||
if (!isOpenApi) {
|
||||
return await AliUser.ApiTokenRefreshAccount(token, true, true).then((isLogin: boolean) => {
|
||||
@@ -125,6 +133,13 @@ export default class AliHttp {
|
||||
|| data.code == 'UserDeviceOffline'
|
||||
|| data.code == 'DeviceSessionSignatureInvalid') {
|
||||
if (token) {
|
||||
if (isDrive115User(token) || isBaiduUser(token) || isCloud123User(token)) {
|
||||
return {
|
||||
code: error.response.status || 403,
|
||||
header: JSON.stringify(error.response.headers || {}),
|
||||
body: error.response.data || 'NetError 当前网盘接口请求错误'
|
||||
} as IUrlRespData
|
||||
}
|
||||
return await AliUser.ApiSessionRefreshAccount(token, true, true).then((flag: boolean) => {
|
||||
if (flag) {
|
||||
return { code: 401, header: '', body: '' } as IUrlRespData
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface IAliFileItem {
|
||||
}
|
||||
|
||||
user_meta?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -295,12 +296,14 @@ export interface IAliGetForderSizeModel {
|
||||
export interface IAliGetDirModel {
|
||||
__v_skip: true
|
||||
drive_id: string
|
||||
user_id?: string
|
||||
file_id: string
|
||||
album_id?: string
|
||||
album_type?: string
|
||||
parent_file_id: string
|
||||
name: string
|
||||
namesearch: string
|
||||
path?: string
|
||||
size: number
|
||||
time: number
|
||||
punish_flag?: number
|
||||
@@ -315,6 +318,7 @@ export interface IAliGetFileModel {
|
||||
parent_file_id: string
|
||||
name: string
|
||||
namesearch: string
|
||||
path?: string
|
||||
ext: string
|
||||
mime_type: string
|
||||
mime_extension: string
|
||||
|
||||
@@ -130,6 +130,203 @@ export default class AliDirFileList {
|
||||
return add
|
||||
}
|
||||
|
||||
private static async _ApiDirFileListOnePageOpenApi(orderby: string, order: string, dir: IAliFileResp, type: string, pageIndex: number, search= true): Promise<boolean> {
|
||||
let url = 'adrive/v1.0/openFile/list'
|
||||
let postData
|
||||
if (useSettingStore().uiFileListMode === 'movie' && search) {
|
||||
postData = {
|
||||
drive_id: dir.m_drive_id,
|
||||
parent_file_id: (dir.dirID === 'resource_root' || dir.dirID === 'backup_root') ? 'root' : dir.dirID,
|
||||
marker: dir.next_marker,
|
||||
category: "video",
|
||||
limit: 200,
|
||||
order_by: orderby,
|
||||
order_direction: order.toUpperCase()
|
||||
}
|
||||
} else {
|
||||
postData = {
|
||||
drive_id: dir.m_drive_id,
|
||||
parent_file_id: (dir.dirID === 'resource_root' || dir.dirID === 'backup_root') ? 'root' : dir.dirID,
|
||||
marker: dir.next_marker,
|
||||
limit: 200,
|
||||
order_by: orderby,
|
||||
order_direction: order.toUpperCase()
|
||||
}
|
||||
}
|
||||
if (type) {
|
||||
postData = Object.assign(postData, { type })
|
||||
pageIndex = -1
|
||||
}
|
||||
const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex, type)
|
||||
}
|
||||
|
||||
static async _ApiSearchFileListOnePage(orderby: string, order: string, dir: IAliFileResp, pageIndex: number): Promise<boolean> {
|
||||
let url = 'adrive/v1.0/openFile/search'
|
||||
let query = ''
|
||||
if (dir.dirID.startsWith('color')) {
|
||||
const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
query = 'description="' + color + '"'
|
||||
} else if (dir.dirID.startsWith('search')) {
|
||||
const search = dir.dirID.substring('search'.length).split(' ')
|
||||
|
||||
let word = ''
|
||||
for (let i = 0; i < search.length; i++) {
|
||||
const itemstr = search[i]
|
||||
if (itemstr.split(':').length !== 2) {
|
||||
word += itemstr + ' '
|
||||
continue
|
||||
}
|
||||
|
||||
const kv = search[i].split(':')
|
||||
const k = kv[0]
|
||||
const v = kv[1]
|
||||
if (k == 'type') {
|
||||
const arr = v.split(',')
|
||||
let type = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
}
|
||||
type = type.substring(0, type.length - 4).trim()
|
||||
if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
else if (type) query += type + ' and '
|
||||
}
|
||||
// else if (k == 'size') {
|
||||
// const size = parseInt(v)
|
||||
// if (size > 0) query += 'size = ' + v + ' and '
|
||||
// }
|
||||
// else if (k == 'max') {
|
||||
// const max = parseInt(v)
|
||||
// if (max > 0) query += 'size <= ' + v + ' and '
|
||||
// } else if (k == 'min') {
|
||||
// const min = parseInt(v)
|
||||
// if (min > 0) query += 'size >= ' + v + ' and '
|
||||
// }
|
||||
else if (k == 'begin') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'created_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'end') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'created_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'ext') {
|
||||
const arr = v.split(',')
|
||||
let extin = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
extin += '"' + arr[j] + '",'
|
||||
}
|
||||
if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
} else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
}
|
||||
word = word.trim()
|
||||
if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
}
|
||||
const postData = {
|
||||
drive_id: dir.m_drive_id,
|
||||
marker: dir.next_marker,
|
||||
limit: 100 ,
|
||||
fields: '*',
|
||||
query: query,
|
||||
order_by: orderby + ' ' + order
|
||||
}
|
||||
const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex)
|
||||
}
|
||||
|
||||
static async _ApiSearchFileListCount(dir: IAliFileResp): Promise<number> {
|
||||
const url = 'adrive/v1.0/openFile/search'
|
||||
|
||||
let query = ''
|
||||
if (dir.dirID.startsWith('color')) {
|
||||
const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
query = 'description="' + color + '"'
|
||||
} else if (dir.dirID.startsWith('search')) {
|
||||
const search = dir.dirID.substring('search'.length).split(' ')
|
||||
|
||||
let word = ''
|
||||
for (let i = 0; i < search.length; i++) {
|
||||
const itemstr = search[i]
|
||||
if (itemstr.split(':').length !== 2) {
|
||||
word += itemstr + ' '
|
||||
continue
|
||||
}
|
||||
|
||||
const kv = search[i].split(':')
|
||||
const k = kv[0]
|
||||
const v = kv[1]
|
||||
if (k == 'type') {
|
||||
const arr = v.split(',')
|
||||
let type = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
}
|
||||
type = type.substring(0, type.length - 4).trim()
|
||||
if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
else if (type) query += type + ' and '
|
||||
}
|
||||
// else if (k == 'size') {
|
||||
// const size = parseInt(v)
|
||||
// if (size > 0) query += 'size = ' + v + ' and '
|
||||
// } else if (k == 'max') {
|
||||
// const max = parseInt(v)
|
||||
// if (max > 0) query += 'size <= ' + v + ' and '
|
||||
// } else if (k == 'min') {
|
||||
// const min = parseInt(v)
|
||||
// if (min > 0) query += 'size >= ' + v + ' and '
|
||||
// }
|
||||
else if (k == 'begin') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'created_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'end') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'created_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'ext') {
|
||||
const arr = v.split(',')
|
||||
let extin = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
extin += '"' + arr[j] + '",'
|
||||
}
|
||||
if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
} else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
}
|
||||
word = word.trim()
|
||||
if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
}
|
||||
const postData = {
|
||||
drive_id: dir.m_drive_id,
|
||||
marker: dir.next_marker,
|
||||
limit: 1 ,
|
||||
fields: '*',
|
||||
query: query,
|
||||
return_total_count: true
|
||||
}
|
||||
const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
try {
|
||||
if (AliHttp.IsSuccess(resp.code)) {
|
||||
return (resp.body.total_count as number) || 0
|
||||
} else {
|
||||
DebugLog.mSaveWarning('_ApiSearchFileListCount err=' + dir.dirID + ' ' + (resp.code || ''))
|
||||
}
|
||||
} catch (err: any) {
|
||||
DebugLog.mSaveDanger('_ApiSearchFileListCount ' + dir.dirID, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// static async _ApiVideoListRecent(orderby: string, order: string, dir: IAliFileResp, pageIndex: number): Promise<boolean> {
|
||||
// const url = 'adrive/v1.0/openFile/video/recentList'
|
||||
// const postData = {}
|
||||
// const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
// return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex)
|
||||
// }
|
||||
|
||||
|
||||
static async ApiDirFileList(user_id: string, drive_id: string, dirID: string, dirName: string, order: string, type: string = '', albumID?: string, refresh: boolean = true): Promise<IAliFileResp> {
|
||||
const dir: IAliFileResp = {
|
||||
@@ -239,7 +436,7 @@ export default class AliDirFileList {
|
||||
}
|
||||
let postData: any = {
|
||||
drive_id: dir.m_drive_id,
|
||||
parent_file_id: dir.dirID.includes('root') ? 'root' : dir.dirID,
|
||||
parent_file_id: (dir.dirID === 'resource_root' || dir.dirID === 'backup_root' || dir.dirID.includes('root')) ? 'root' : dir.dirID,
|
||||
marker: dir.next_marker,
|
||||
limit: 100,
|
||||
all: false,
|
||||
@@ -266,7 +463,7 @@ export default class AliDirFileList {
|
||||
} else {
|
||||
url = 'adrive/v3/file/search'
|
||||
}
|
||||
let parent_file_id = dir.dirID.includes('_root') ? 'root' : dir.dirID
|
||||
let parent_file_id = (dir.dirID === 'resource_root' || dir.dirID === 'backup_root' || dir.dirID.includes('_root')) ? 'root' : dir.dirID
|
||||
const postData: any = {
|
||||
drive_id: dir.m_drive_id,
|
||||
marker: '',
|
||||
@@ -370,177 +567,177 @@ export default class AliDirFileList {
|
||||
return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex)
|
||||
}
|
||||
|
||||
static async _ApiSearchFileListOnePage(orderby: string, order: string, dir: IAliFileResp, pageIndex: number): Promise<boolean> {
|
||||
let url = 'adrive/v3/file/search'
|
||||
if (useSettingStore().uiShowPanMedia == false) url += '?jsonmask=next_marker%2Citems(' + AliDirFileList.ItemJsonmask + ')'
|
||||
else url += '?jsonmask=next_marker%2Citems(' + AliDirFileList.ItemJsonmask + '%2Cuser_meta%2Cvideo_media_metadata(duration%2Cwidth%2Cheight%2Ctime)%2Cvideo_preview_metadata%2Fduration%2Cimage_media_metadata)'
|
||||
// static async _ApiSearchFileListOnePage(orderby: string, order: string, dir: IAliFileResp, pageIndex: number): Promise<boolean> {
|
||||
// let url = 'adrive/v3/file/search'
|
||||
// if (useSettingStore().uiShowPanMedia == false) url += '?jsonmask=next_marker%2Citems(' + AliDirFileList.ItemJsonmask + ')'
|
||||
// else url += '?jsonmask=next_marker%2Citems(' + AliDirFileList.ItemJsonmask + '%2Cuser_meta%2Cvideo_media_metadata(duration%2Cwidth%2Cheight%2Ctime)%2Cvideo_preview_metadata%2Fduration%2Cimage_media_metadata)'
|
||||
//
|
||||
// let query = ''
|
||||
// let drive_id_list = []
|
||||
// if (dir.dirID.startsWith('color')) {
|
||||
// const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
// query = 'description="' + color + '"'
|
||||
// } else if (dir.dirID.startsWith('search')) {
|
||||
// const search = dir.dirID.substring('search'.length).split(' ')
|
||||
// let word = ''
|
||||
// for (let i = 0; i < search.length; i++) {
|
||||
// const itemstr = search[i]
|
||||
// if (itemstr.split(':').length !== 2) {
|
||||
// word += itemstr + ' '
|
||||
// continue
|
||||
// }
|
||||
// const kv = search[i].split(':')
|
||||
// const k = kv[0]
|
||||
// const v = kv[1]
|
||||
// if (k == 'range') {
|
||||
// const arr = v.split(',')
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// drive_id_list.push(GetDriveID(dir.m_user_id, arr[j]))
|
||||
// }
|
||||
// } else if (k == 'type') {
|
||||
// const arr = v.split(',')
|
||||
// let type = ''
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
// else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
// }
|
||||
// type = type.substring(0, type.length - 4).trim()
|
||||
// if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
// else if (type) query += type + ' and '
|
||||
// } else if (k == 'size') {
|
||||
// const size = parseInt(v)
|
||||
// if (size > 0) query += 'size = ' + v + ' and '
|
||||
// } else if (k == 'description') {
|
||||
// query += 'description = ' + v + ' and '
|
||||
// } else if (k == 'max') {
|
||||
// const max = parseInt(v)
|
||||
// if (max > 0) query += 'size <= ' + v + ' and '
|
||||
// } else if (k == 'min') {
|
||||
// const min = parseInt(v)
|
||||
// if (min > 0) query += 'size >= ' + v + ' and '
|
||||
// } else if (k == 'begin') {
|
||||
// const dt = new Date(v).toISOString()
|
||||
// query += 'updated_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
// } else if (k == 'end') {
|
||||
// const dt = new Date(v).toISOString()
|
||||
// query += 'updated_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
// } else if (k == 'ext') {
|
||||
// const arr = v.split(',')
|
||||
// let extin = ''
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// extin += '"' + arr[j] + '",'
|
||||
// }
|
||||
// if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
// if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
// } else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
// }
|
||||
// word = word.trim()
|
||||
// if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
// if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
// if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
// }
|
||||
// const postData: any = {
|
||||
// marker: dir.next_marker,
|
||||
// limit: 100,
|
||||
// fields: '*',
|
||||
// query: query,
|
||||
// order_by: orderby + ' ' + order
|
||||
// }
|
||||
// if (drive_id_list.length > 0) postData.drive_id_list = drive_id_list
|
||||
// else postData.drive_id = dir.m_drive_id
|
||||
// const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
// return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex)
|
||||
// }
|
||||
|
||||
let query = ''
|
||||
let drive_id_list = []
|
||||
if (dir.dirID.startsWith('color')) {
|
||||
const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
query = 'description="' + color + '"'
|
||||
} else if (dir.dirID.startsWith('search')) {
|
||||
const search = dir.dirID.substring('search'.length).split(' ')
|
||||
let word = ''
|
||||
for (let i = 0; i < search.length; i++) {
|
||||
const itemstr = search[i]
|
||||
if (itemstr.split(':').length !== 2) {
|
||||
word += itemstr + ' '
|
||||
continue
|
||||
}
|
||||
const kv = search[i].split(':')
|
||||
const k = kv[0]
|
||||
const v = kv[1]
|
||||
if (k == 'range') {
|
||||
const arr = v.split(',')
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
drive_id_list.push(GetDriveID(dir.m_user_id, arr[j]))
|
||||
}
|
||||
} else if (k == 'type') {
|
||||
const arr = v.split(',')
|
||||
let type = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
}
|
||||
type = type.substring(0, type.length - 4).trim()
|
||||
if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
else if (type) query += type + ' and '
|
||||
} else if (k == 'size') {
|
||||
const size = parseInt(v)
|
||||
if (size > 0) query += 'size = ' + v + ' and '
|
||||
} else if (k == 'description') {
|
||||
query += 'description = ' + v + ' and '
|
||||
} else if (k == 'max') {
|
||||
const max = parseInt(v)
|
||||
if (max > 0) query += 'size <= ' + v + ' and '
|
||||
} else if (k == 'min') {
|
||||
const min = parseInt(v)
|
||||
if (min > 0) query += 'size >= ' + v + ' and '
|
||||
} else if (k == 'begin') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'updated_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'end') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'updated_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'ext') {
|
||||
const arr = v.split(',')
|
||||
let extin = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
extin += '"' + arr[j] + '",'
|
||||
}
|
||||
if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
} else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
}
|
||||
word = word.trim()
|
||||
if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
}
|
||||
const postData: any = {
|
||||
marker: dir.next_marker,
|
||||
limit: 100,
|
||||
fields: '*',
|
||||
query: query,
|
||||
order_by: orderby + ' ' + order
|
||||
}
|
||||
if (drive_id_list.length > 0) postData.drive_id_list = drive_id_list
|
||||
else postData.drive_id = dir.m_drive_id
|
||||
const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
return AliDirFileList._FileListOnePage(orderby, order, dir, resp, pageIndex)
|
||||
}
|
||||
|
||||
static async _ApiSearchFileListCount(dir: IAliFileResp): Promise<number> {
|
||||
const url = 'adrive/v3/file/search'
|
||||
let query = ''
|
||||
let drive_id_list = []
|
||||
if (dir.dirID.startsWith('color')) {
|
||||
const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
query = 'description="' + color + '"'
|
||||
} else if (dir.dirID.startsWith('search')) {
|
||||
const search = dir.dirID.substring('search'.length).split(' ')
|
||||
|
||||
let word = ''
|
||||
for (let i = 0; i < search.length; i++) {
|
||||
const itemstr = search[i]
|
||||
if (itemstr.split(':').length !== 2) {
|
||||
word += itemstr + ' '
|
||||
continue
|
||||
}
|
||||
|
||||
const kv = search[i].split(':')
|
||||
const k = kv[0]
|
||||
const v = kv[1]
|
||||
if (k == 'range') {
|
||||
const arr = v.split(',')
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
drive_id_list.push(GetDriveID(dir.m_user_id, arr[j]))
|
||||
}
|
||||
} else if (k == 'type') {
|
||||
const arr = v.split(',')
|
||||
let type = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
}
|
||||
type = type.substring(0, type.length - 4).trim()
|
||||
if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
else if (type) query += type + ' and '
|
||||
} else if (k == 'size') {
|
||||
const size = parseInt(v)
|
||||
if (size > 0) query += 'size = ' + v + ' and '
|
||||
} else if (k == 'description') {
|
||||
query += 'description = ' + v + ' and '
|
||||
} else if (k == 'max') {
|
||||
const max = parseInt(v)
|
||||
if (max > 0) query += 'size <= ' + v + ' and '
|
||||
} else if (k == 'min') {
|
||||
const min = parseInt(v)
|
||||
if (min > 0) query += 'size >= ' + v + ' and '
|
||||
} else if (k == 'begin') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'updated_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'end') {
|
||||
const dt = new Date(v).toISOString()
|
||||
query += 'updated_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
} else if (k == 'ext') {
|
||||
const arr = v.split(',')
|
||||
let extin = ''
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
extin += '"' + arr[j] + '",'
|
||||
}
|
||||
if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
} else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
}
|
||||
word = word.trim()
|
||||
if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
}
|
||||
const postData: any = {
|
||||
marker: dir.next_marker,
|
||||
limit: 1,
|
||||
fields: '*',
|
||||
query: query,
|
||||
return_total_count: true
|
||||
}
|
||||
if (drive_id_list.length > 0) postData.drive_id_list = drive_id_list
|
||||
else postData.drive_id = dir.m_drive_id
|
||||
const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
try {
|
||||
if (AliHttp.IsSuccess(resp.code)) {
|
||||
return (resp.body.total_count as number) || 0
|
||||
} else if (!AliHttp.HttpCodeBreak(resp.code)) {
|
||||
DebugLog.mSaveWarning('_ApiSearchFileListCount err=' + dir.dirID + ' ' + (resp.code || ''), resp.body)
|
||||
}
|
||||
} catch (err: any) {
|
||||
DebugLog.mSaveDanger('_ApiSearchFileListCount ' + dir.dirID, err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
// static async _ApiSearchFileListCount(dir: IAliFileResp): Promise<number> {
|
||||
// const url = 'adrive/v3/file/search'
|
||||
// let query = ''
|
||||
// let drive_id_list = []
|
||||
// if (dir.dirID.startsWith('color')) {
|
||||
// const color = dir.dirID.substring('color'.length).split(' ')[0].replace('#', 'c')
|
||||
// query = 'description="' + color + '"'
|
||||
// } else if (dir.dirID.startsWith('search')) {
|
||||
// const search = dir.dirID.substring('search'.length).split(' ')
|
||||
//
|
||||
// let word = ''
|
||||
// for (let i = 0; i < search.length; i++) {
|
||||
// const itemstr = search[i]
|
||||
// if (itemstr.split(':').length !== 2) {
|
||||
// word += itemstr + ' '
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// const kv = search[i].split(':')
|
||||
// const k = kv[0]
|
||||
// const v = kv[1]
|
||||
// if (k == 'range') {
|
||||
// const arr = v.split(',')
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// drive_id_list.push(GetDriveID(dir.m_user_id, arr[j]))
|
||||
// }
|
||||
// } else if (k == 'type') {
|
||||
// const arr = v.split(',')
|
||||
// let type = ''
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// if (arr[j] == 'folder') type += 'type="' + arr[j] + '" or '
|
||||
// else if (arr[j]) type += 'category="' + arr[j] + '" or '
|
||||
// }
|
||||
// type = type.substring(0, type.length - 4).trim()
|
||||
// if (type && type.indexOf(' or ') > 0) query += '(' + type + ') and '
|
||||
// else if (type) query += type + ' and '
|
||||
// } else if (k == 'size') {
|
||||
// const size = parseInt(v)
|
||||
// if (size > 0) query += 'size = ' + v + ' and '
|
||||
// } else if (k == 'description') {
|
||||
// query += 'description = ' + v + ' and '
|
||||
// } else if (k == 'max') {
|
||||
// const max = parseInt(v)
|
||||
// if (max > 0) query += 'size <= ' + v + ' and '
|
||||
// } else if (k == 'min') {
|
||||
// const min = parseInt(v)
|
||||
// if (min > 0) query += 'size >= ' + v + ' and '
|
||||
// } else if (k == 'begin') {
|
||||
// const dt = new Date(v).toISOString()
|
||||
// query += 'updated_at >= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
// } else if (k == 'end') {
|
||||
// const dt = new Date(v).toISOString()
|
||||
// query += 'updated_at <= "' + dt.substring(0, dt.lastIndexOf('.')) + '" and '
|
||||
// } else if (k == 'ext') {
|
||||
// const arr = v.split(',')
|
||||
// let extin = ''
|
||||
// for (let j = 0; j < arr.length; j++) {
|
||||
// extin += '"' + arr[j] + '",'
|
||||
// }
|
||||
// if (extin.length > 0) extin = extin.substring(0, extin.length - 1)
|
||||
// if (extin) query += 'file_extension in [' + extin + '] and '
|
||||
// } else if (k == 'fav') query += 'starred = ' + v + ' and '
|
||||
// }
|
||||
// word = word.trim()
|
||||
// if (word) query += 'name match "' + word.replaceAll('"', '\\"') + '" and '
|
||||
// if (query.length > 0) query = query.substring(0, query.length - 5)
|
||||
// if (query.startsWith('(') && query.endsWith(')')) query = query.substring(1, query.length - 1)
|
||||
// }
|
||||
// const postData: any = {
|
||||
// marker: dir.next_marker,
|
||||
// limit: 1,
|
||||
// fields: '*',
|
||||
// query: query,
|
||||
// return_total_count: true
|
||||
// }
|
||||
// if (drive_id_list.length > 0) postData.drive_id_list = drive_id_list
|
||||
// else postData.drive_id = dir.m_drive_id
|
||||
// const resp = await AliHttp.Post(url, postData, dir.m_user_id, '')
|
||||
// try {
|
||||
// if (AliHttp.IsSuccess(resp.code)) {
|
||||
// return (resp.body.total_count as number) || 0
|
||||
// } else if (!AliHttp.HttpCodeBreak(resp.code)) {
|
||||
// DebugLog.mSaveWarning('_ApiSearchFileListCount err=' + dir.dirID + ' ' + (resp.code || ''), resp.body)
|
||||
// }
|
||||
// } catch (err: any) {
|
||||
// DebugLog.mSaveDanger('_ApiSearchFileListCount ' + dir.dirID, err)
|
||||
// }
|
||||
// return 0
|
||||
// }
|
||||
|
||||
static async _ApiAlbumListOnePage(orderby: string, order: string, dir: IAliFileResp, pageIndex: number): Promise<boolean> {
|
||||
const url = 'adrive/v1/album/list'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from 'path'
|
||||
import { useSettingStore } from '../store'
|
||||
import DebugLog from '../utils/debuglog'
|
||||
import { GetExpiresTime, HanToPin } from '../utils/utils'
|
||||
@@ -5,13 +6,100 @@ import AliHttp from './alihttp'
|
||||
import { IAliFileItem, IAliGetDirModel, IAliGetFileModel, IAliGetForderSizeModel } from './alimodels'
|
||||
import AliDirFileList from './dirfilelist'
|
||||
import { ICompilationList, IDownloadUrl, IOfficePreViewUrl, IVideoPreviewUrl, IVideoXBTUrl } from './models'
|
||||
import { DecodeEncName, GetDriveType } from './utils'
|
||||
import { DecodeEncName, GetDriveType, isAliyunUser, isBaiduUser, isCloud123User, isDrive115User } from './utils'
|
||||
import { getRawUrl } from '../utils/proxyhelper'
|
||||
import { apiCloud123DownloadInfo, apiCloud123FileDetail } from '../cloud123/filecmd'
|
||||
import { mapCloud123InfoToAliModel } from '../cloud123/dirfilelist'
|
||||
import { apiCloud123TranscodeList } from '../cloud123/video'
|
||||
import { apiDrive115FileDetail } from '../cloud115/filecmd'
|
||||
import { apiDrive115DownUrl } from '../cloud115/download'
|
||||
import { mapDrive115DetailToAliModel } from '../cloud115/dirfilelist'
|
||||
import { apiDrive115VideoHistoryUpdate, apiDrive115VideoPlay, getDrive115PickCode } from '../cloud115/video'
|
||||
import { apiBaiduFileList } from '../cloudbaidu/dirfilelist'
|
||||
import { apiBaiduFileMetas, mapBaiduMetaToAliFileItem } from '../cloudbaidu/filecmd'
|
||||
import TreeStore from '../store/treestore'
|
||||
import UserDAL from '../user/userdal'
|
||||
import { getWebDavConnection, getWebDavConnectionId, getWebDavDownloadUrl, isWebDavDrive } from '../utils/webdavClient'
|
||||
|
||||
const parseBaiduPath = (file_path: string) => {
|
||||
let p = file_path || '/'
|
||||
if (!p.startsWith('/')) p = '/' + p
|
||||
const idx = p.lastIndexOf('/')
|
||||
const parent = idx <= 0 ? '/' : p.substring(0, idx)
|
||||
const name = p.substring(idx + 1)
|
||||
return { parent, name }
|
||||
}
|
||||
|
||||
const getBaiduMetaByPath = async (user_id: string, file_id: string) => {
|
||||
if (!file_id) return null
|
||||
const fsid = Number(file_id)
|
||||
if (!Number.isFinite(fsid)) return null
|
||||
const metas = await apiBaiduFileMetas(user_id, [fsid], 1)
|
||||
if (!metas || metas.length === 0) return null
|
||||
return { meta: metas[0], fs_id: fsid, size: Number(metas[0].size || 0) }
|
||||
}
|
||||
|
||||
export default class AliFile {
|
||||
|
||||
static async ApiFileInfo(user_id: string, drive_id: string, file_id: string, ispic: boolean = false): Promise<any | undefined> {
|
||||
if (!drive_id || !file_id) return undefined
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(drive_id))
|
||||
const normalizedPath = file_id === 'root' ? '/' : file_id
|
||||
if (normalizedPath === '/') {
|
||||
return {
|
||||
drive_id,
|
||||
file_id: '/',
|
||||
parent_file_id: '',
|
||||
name: connection?.name || 'WebDAV',
|
||||
type: 'folder',
|
||||
isDir: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
drive_id,
|
||||
file_id: normalizedPath,
|
||||
parent_file_id: path.posix.dirname(normalizedPath) || '/',
|
||||
name: path.posix.basename(normalizedPath) || connection?.name || 'WebDAV',
|
||||
type: 'folder',
|
||||
isDir: true
|
||||
}
|
||||
}
|
||||
if (!user_id || !drive_id || !file_id) return undefined
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
const detail = await apiCloud123FileDetail(user_id, file_id)
|
||||
if (!detail) return undefined
|
||||
const mapped = mapCloud123InfoToAliModel(detail) as any
|
||||
mapped.type = mapped.isDir ? 'folder' : 'file'
|
||||
return mapped
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
const detail = await apiDrive115FileDetail(user_id, file_id)
|
||||
if (!detail) return undefined
|
||||
const mapped = mapDrive115DetailToAliModel(detail, drive_id) as any
|
||||
mapped.type = mapped.isDir ? 'folder' : 'file'
|
||||
mapped.pick_code = detail.pick_code
|
||||
return mapped
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
if (file_id === 'baidu_root' || file_id === '/') {
|
||||
return {
|
||||
drive_id,
|
||||
file_id,
|
||||
parent_file_id: '',
|
||||
name: '网盘文件',
|
||||
type: 'folder',
|
||||
isDir: true
|
||||
}
|
||||
}
|
||||
const metaInfo = await getBaiduMetaByPath(user_id, file_id)
|
||||
if (metaInfo?.meta) {
|
||||
const mapped = mapBaiduMetaToAliFileItem(metaInfo.meta, drive_id, file_id) as any
|
||||
mapped.type = mapped.isDir ? 'folder' : 'file'
|
||||
return mapped
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
let url = ''
|
||||
let postData = {}
|
||||
if (!ispic) {
|
||||
@@ -84,7 +172,62 @@ export default class AliFile {
|
||||
}
|
||||
|
||||
static async ApiFileDownloadUrl(user_id: string, drive_id: string, file_id: string, expire_sec: number): Promise<IDownloadUrl | string> {
|
||||
if (!drive_id || !file_id) return '参数错误'
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connectionId = getWebDavConnectionId(drive_id)
|
||||
const connection = getWebDavConnection(connectionId)
|
||||
if (!connection) return 'WebDAV 连接不存在,请重新连接'
|
||||
const url = await getWebDavDownloadUrl(connection, file_id)
|
||||
console.log(`WebDAV download url ${url}`)
|
||||
return {
|
||||
drive_id,
|
||||
file_id,
|
||||
expire_time: 0,
|
||||
url,
|
||||
size: 0
|
||||
}
|
||||
}
|
||||
if (!user_id || !drive_id || !file_id) return '参数错误'
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
const metaInfo = await getBaiduMetaByPath(user_id, file_id)
|
||||
if (!metaInfo?.meta?.dlink) return '获取下载地址失败'
|
||||
let dlink = metaInfo.meta.dlink
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (token?.access_token && !dlink.includes('access_token=')) {
|
||||
dlink += (dlink.includes('?') ? '&' : '?') + `access_token=${token.access_token}`
|
||||
}
|
||||
return {
|
||||
drive_id,
|
||||
file_id,
|
||||
expire_time: GetExpiresTime(dlink),
|
||||
url: dlink,
|
||||
size: Number(metaInfo.meta.size || metaInfo.size || 0)
|
||||
}
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
const data = await apiCloud123DownloadInfo(user_id, file_id)
|
||||
if (typeof data === 'string') return data
|
||||
return {
|
||||
drive_id: drive_id,
|
||||
file_id: file_id,
|
||||
expire_time: 0,
|
||||
url: data.url,
|
||||
size: 0
|
||||
}
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
const detail = await apiDrive115FileDetail(user_id, file_id)
|
||||
if (!detail) return '获取文件详情失败'
|
||||
const down = await apiDrive115DownUrl(user_id, detail.pick_code)
|
||||
if (typeof down === 'string') return down
|
||||
return {
|
||||
drive_id: drive_id,
|
||||
file_id: file_id,
|
||||
expire_time: GetExpiresTime(down.url),
|
||||
url: down.url,
|
||||
size: down.size || detail.size || 0
|
||||
}
|
||||
}
|
||||
const data: IDownloadUrl = {
|
||||
drive_id: drive_id,
|
||||
file_id: file_id,
|
||||
@@ -129,7 +272,118 @@ export default class AliFile {
|
||||
}
|
||||
|
||||
static async ApiVideoPreviewUrl(user_id: string, drive_id: string, file_id: string): Promise<IVideoPreviewUrl | string> {
|
||||
if (!drive_id || !file_id) return '参数错误'
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
return '暂无转码信息'
|
||||
}
|
||||
if (!user_id || !drive_id || !file_id) return '参数错误'
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
return '暂无转码信息'
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
const transcode = await apiCloud123TranscodeList(user_id, file_id)
|
||||
if (typeof transcode === 'string') return transcode
|
||||
if (!transcode.list.length) {
|
||||
if (transcode.status === 1) return '视频正在转码中,稍后重试'
|
||||
if (transcode.status === 3) return '视频转码失败'
|
||||
return '暂无转码信息'
|
||||
}
|
||||
const data: IVideoPreviewUrl = {
|
||||
drive_id: drive_id,
|
||||
file_id: file_id,
|
||||
size: 0,
|
||||
expire_time: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
duration: 0,
|
||||
qualities: [],
|
||||
subtitles: []
|
||||
}
|
||||
data.qualities = transcode.list
|
||||
.filter(item => item && item.url)
|
||||
.map(item => {
|
||||
const label = item.resolution || (item.height ? `${item.height}p` : '清晰度')
|
||||
return {
|
||||
html: label,
|
||||
quality: label,
|
||||
height: Number(item.height || 0),
|
||||
width: 0,
|
||||
label,
|
||||
value: label,
|
||||
url: item.url
|
||||
}
|
||||
})
|
||||
data.qualities = data.qualities.sort((a, b) => (b.height || 0) - (a.height || 0))
|
||||
if (data.qualities.length > 0) {
|
||||
const first = data.qualities[0]
|
||||
data.height = first.height || 0
|
||||
data.expire_time = GetExpiresTime(first.url)
|
||||
}
|
||||
const duration = transcode.list.find(item => item.duration)?.duration
|
||||
data.duration = Math.floor(Number(duration || 0))
|
||||
return data
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
const meta = await getDrive115PickCode(user_id, file_id)
|
||||
if (!meta?.pick_code) return '获取文件详情失败'
|
||||
const playInfo = await apiDrive115VideoPlay(user_id, meta.pick_code)
|
||||
if (typeof playInfo === 'string') return playInfo
|
||||
const data: IVideoPreviewUrl = {
|
||||
drive_id: drive_id,
|
||||
file_id: file_id,
|
||||
size: 0,
|
||||
expire_time: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
duration: 0,
|
||||
qualities: [],
|
||||
subtitles: []
|
||||
}
|
||||
const defLabel = (def: string) => {
|
||||
switch (def) {
|
||||
case '1': return '标清'
|
||||
case '2': return '高清'
|
||||
case '3': return '超清'
|
||||
case '4': return '1080P'
|
||||
case '5': return '4K'
|
||||
case '100': return '原画'
|
||||
default: return def ? `清晰度${def}` : '清晰度'
|
||||
}
|
||||
}
|
||||
const list = playInfo.video_url || []
|
||||
data.qualities = list
|
||||
.filter(item => item && item.url)
|
||||
.map(item => {
|
||||
const def = String(item.definition ?? item.definition_n ?? '')
|
||||
const label = item.title || defLabel(def)
|
||||
return {
|
||||
html: label,
|
||||
quality: def,
|
||||
height: Number(item.height || 0),
|
||||
width: Number(item.width || 0),
|
||||
label,
|
||||
value: label,
|
||||
url: item.url
|
||||
}
|
||||
})
|
||||
data.qualities = data.qualities.sort((a, b) => (b.width || 0) - (a.width || 0))
|
||||
const userDef = playInfo.user_def ? String(playInfo.user_def) : ''
|
||||
if (userDef) {
|
||||
const idx = data.qualities.findIndex(q => q.quality === userDef)
|
||||
if (idx > 0) {
|
||||
const [picked] = data.qualities.splice(idx, 1)
|
||||
data.qualities.unshift(picked)
|
||||
}
|
||||
}
|
||||
if (data.qualities.length > 0) {
|
||||
const first = data.qualities[0]
|
||||
data.width = first.width || 0
|
||||
data.height = first.height || 0
|
||||
data.expire_time = GetExpiresTime(first.url)
|
||||
}
|
||||
data.duration = Math.floor(Number(playInfo.play_long || meta.play_long || 0))
|
||||
return data
|
||||
}
|
||||
let url = ''
|
||||
let need_open_api = true
|
||||
if (need_open_api) {
|
||||
@@ -384,6 +638,11 @@ export default class AliFile {
|
||||
|
||||
static async ApiFileGetPathString(user_id: string, drive_id: string, file_id: string, dirsplit: string): Promise<string> {
|
||||
if (!user_id || !drive_id || !file_id) return ''
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
const pathList = TreeStore.GetDirPath(drive_id, file_id)
|
||||
const pathNames = pathList.map((item) => item.name).filter((name) => name)
|
||||
return pathNames.join(dirsplit)
|
||||
}
|
||||
if (file_id.includes('root')) {
|
||||
if (file_id.startsWith('backup')) {
|
||||
return '备份盘'
|
||||
@@ -416,6 +675,9 @@ export default class AliFile {
|
||||
|
||||
static async ApiFileGetFolderSize(user_id: string, drive_id: string, file_id: string): Promise<IAliGetForderSizeModel | undefined> {
|
||||
if (!user_id || !drive_id || !file_id) return undefined
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
return { size: 0, folder_count: 0, file_count: 0, reach_limit: undefined }
|
||||
}
|
||||
const url = 'adrive/v1/file/get_folder_size_info'
|
||||
|
||||
const postData = {
|
||||
@@ -523,6 +785,18 @@ export default class AliFile {
|
||||
static async ApiUpdateVideoTime(user_id: string, drive_id: string, file_id: string, play_cursor: number): Promise<IAliFileItem | undefined> {
|
||||
if (!useSettingStore().uiAutoPlaycursorVideo) return
|
||||
if (!user_id || !drive_id || !file_id) return undefined
|
||||
if (isWebDavDrive(drive_id)) return undefined
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') return undefined
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') return undefined
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
const meta = await getDrive115PickCode(user_id, file_id)
|
||||
if (!meta?.pick_code) return undefined
|
||||
const playLong = Number(meta.play_long || 0)
|
||||
const watch_end = playLong > 0 && play_cursor >= playLong - 10 ? 1 : 0
|
||||
await apiDrive115VideoHistoryUpdate(user_id, meta.pick_code, play_cursor, watch_end)
|
||||
return undefined
|
||||
}
|
||||
if (!isAliyunUser(user_id)) return undefined
|
||||
let url = ''
|
||||
let need_open_api = true
|
||||
if (need_open_api) {
|
||||
|
||||
@@ -2,13 +2,55 @@ import DebugLog from '../utils/debuglog'
|
||||
import AliHttp from './alihttp'
|
||||
import { IAliFileItem, IAliGetFileModel } from './alimodels'
|
||||
import AliDirFileList from './dirfilelist'
|
||||
import { ApiBatch, ApiBatchMaker, ApiBatchMaker2, ApiBatchSuccess, EncodeEncName } from './utils'
|
||||
import { ApiBatch, ApiBatchMaker, ApiBatchMaker2, ApiBatchSuccess, EncodeEncName, isBaiduUser, isCloud123User, isDrive115User } from './utils'
|
||||
import { IDownloadUrl } from './models'
|
||||
import AliFile from './file'
|
||||
import message from '../utils/message'
|
||||
import usePanFileStore from '../pan/panfilestore'
|
||||
import usePanTreeStore from '../pan/pantreestore'
|
||||
import { apiCloud123CopyBatch, apiCloud123CopySingle, apiCloud123DeleteBatch, apiCloud123FileDetail, apiCloud123FileInfos, apiCloud123Mkdir, apiCloud123MoveBatch, apiCloud123RecoverBatch, apiCloud123TrashBatch } from '../cloud123/filecmd'
|
||||
import { mapCloud123InfoToAliModel } from '../cloud123/dirfilelist'
|
||||
import { apiDrive115Mkdir } from '../cloud115/filecmd'
|
||||
import { apiDrive115CopyBatch } from '../cloud115/copy'
|
||||
import { apiDrive115MoveBatch } from '../cloud115/move'
|
||||
import { apiDrive115Rename } from '../cloud115/rename'
|
||||
import { apiDrive115TrashBatch, apiDrive115TrashDelete, apiDrive115TrashRestore } from '../cloud115/trash'
|
||||
import { apiBaiduCopy, apiBaiduDelete, apiBaiduMove, apiBaiduRename } from '../cloudbaidu/filemanager'
|
||||
import { apiBaiduCreateDir, buildBaiduUploadPath } from '../cloudbaidu/upload'
|
||||
import { copyWebDavPath, createWebDavDirectory, deleteWebDavPath, getWebDavConnection, getWebDavConnectionId, isWebDavDrive, moveWebDavPath, normalizeWebDavPath, renameWebDavPath } from '../utils/webdavClient'
|
||||
|
||||
export default class AliFileCmd {
|
||||
static ResolveBaiduPaths(file_idList: string[]): string[] {
|
||||
if (!file_idList.length) return []
|
||||
const list = usePanFileStore().ListDataRaw || []
|
||||
return file_idList.map((id) => {
|
||||
if (id.startsWith('/')) return id
|
||||
const hit: any = list.find((item: any) => item.file_id === id)
|
||||
if (hit?.path) return hit.path
|
||||
const match = hit?.description && /baidu_path:([^;]+)/.exec(hit.description)
|
||||
if (match && match[1]) return match[1]
|
||||
return id
|
||||
})
|
||||
}
|
||||
static ResolveBaiduTargetPath(file_id: string, path: string = '', description: string = ''): string {
|
||||
if (path) return path
|
||||
if (!file_id) return '/'
|
||||
if (file_id.startsWith('/')) return file_id
|
||||
const directMatch = description && /baidu_path:([^;]+)/.exec(description)
|
||||
if (directMatch && directMatch[1]) return directMatch[1]
|
||||
const list = usePanFileStore().ListDataRaw || []
|
||||
const hit: any = list.find((item: any) => item.file_id === file_id)
|
||||
if (hit?.path) return hit.path
|
||||
const listMatch = hit?.description && /baidu_path:([^;]+)/.exec(hit.description)
|
||||
if (listMatch && listMatch[1]) return listMatch[1]
|
||||
const selectDir: any = usePanTreeStore().selectDir
|
||||
if (selectDir?.file_id === file_id) {
|
||||
if (selectDir.path) return selectDir.path
|
||||
const dirMatch = selectDir.description && /baidu_path:([^;]+)/.exec(selectDir.description)
|
||||
if (dirMatch && dirMatch[1]) return dirMatch[1]
|
||||
}
|
||||
return file_id
|
||||
}
|
||||
static async ApiCreatNewForder(
|
||||
user_id: string, drive_id: string,
|
||||
parent_file_id: string, creatDirName: string,
|
||||
@@ -16,6 +58,36 @@ export default class AliFileCmd {
|
||||
): Promise<{ file_id: string; error: string }> {
|
||||
const result = { file_id: '', error: '新建文件夹失败' }
|
||||
if (!user_id || !drive_id || !parent_file_id) return result
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connectionId = getWebDavConnectionId(drive_id)
|
||||
const connection = getWebDavConnection(connectionId)
|
||||
if (!connection) return result
|
||||
const parentPath = parent_file_id.includes('root') ? '/' : parent_file_id
|
||||
const targetPath = normalizeWebDavPath(`${parentPath}/${creatDirName}`)
|
||||
try {
|
||||
await createWebDavDirectory(connection, targetPath)
|
||||
return { file_id: targetPath, error: '' }
|
||||
} catch (error: any) {
|
||||
return { file_id: '', error: error?.message || result.error }
|
||||
}
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
if (parent_file_id.includes('root')) parent_file_id = '0'
|
||||
const resp = await apiCloud123Mkdir(user_id, parent_file_id, creatDirName)
|
||||
return { file_id: resp.file_id, error: resp.error }
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
if (parent_file_id.includes('root')) parent_file_id = '0'
|
||||
const resp = await apiDrive115Mkdir(user_id, parent_file_id, creatDirName)
|
||||
return { file_id: resp.file_id, error: resp.error }
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
if (parent_file_id.includes('root')) parent_file_id = '/'
|
||||
const dirPath = buildBaiduUploadPath(parent_file_id || '/', creatDirName)
|
||||
const rtype = check_name_mode === 'auto_rename' ? 1 : 0
|
||||
const resp = await apiBaiduCreateDir(user_id, dirPath, rtype)
|
||||
return { file_id: resp.path, error: resp.error }
|
||||
}
|
||||
if (parent_file_id.includes('root')) parent_file_id = 'root'
|
||||
const url = 'adrive/v2/file/createWithFolders'
|
||||
const name = EncodeEncName(user_id, creatDirName, true, encType)
|
||||
@@ -41,6 +113,19 @@ export default class AliFileCmd {
|
||||
|
||||
|
||||
static async ApiTrashBatch(user_id: string, drive_id: string, file_idList: string[]): Promise<string[]> {
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
return []
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
return apiCloud123TrashBatch(user_id, file_idList)
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
return apiDrive115TrashBatch(user_id, file_idList)
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
const paths = AliFileCmd.ResolveBaiduPaths(file_idList)
|
||||
return apiBaiduDelete(user_id, paths)
|
||||
}
|
||||
const batchList = ApiBatchMaker('/recyclebin/trash', file_idList, (file_id: string) => {
|
||||
return { drive_id: drive_id, file_id: file_id }
|
||||
})
|
||||
@@ -49,6 +134,35 @@ export default class AliFileCmd {
|
||||
|
||||
|
||||
static async ApiDeleteBatch(user_id: string, drive_id: string, file_idList: string[]): Promise<string[]> {
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(drive_id))
|
||||
if (!connection) {
|
||||
message.error('WebDAV 连接不存在,请重新连接')
|
||||
return []
|
||||
}
|
||||
const successList: string[] = []
|
||||
for (const file_id of file_idList) {
|
||||
try {
|
||||
await deleteWebDavPath(connection, file_id)
|
||||
successList.push(file_id)
|
||||
} catch (error: any) {
|
||||
console.error('WebDAV 删除失败:', error)
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
message.error("暂不支持彻底删除,请移步至官方客户端操作")
|
||||
return []
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
message.error('115网盘不支持直接彻底删除,请先移入回收站后再删除')
|
||||
return []
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
const paths = AliFileCmd.ResolveBaiduPaths(file_idList)
|
||||
return apiBaiduDelete(user_id, paths)
|
||||
}
|
||||
const batchList = ApiBatchMaker('/file/delete', file_idList, (file_id: string) => {
|
||||
return { drive_id: drive_id, file_id: file_id }
|
||||
})
|
||||
@@ -62,6 +176,51 @@ export default class AliFileCmd {
|
||||
name: string;
|
||||
isDir: boolean
|
||||
}[]> {
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(drive_id))
|
||||
if (!connection) return []
|
||||
const successList: { file_id: string; parent_file_id: string; name: string; isDir: boolean }[] = []
|
||||
for (let i = 0, maxi = file_idList.length; i < maxi; i++) {
|
||||
const file_id = file_idList[i]
|
||||
const name = names[i] || ''
|
||||
if (!file_id || !name) continue
|
||||
try {
|
||||
const targetPath = await renameWebDavPath(connection, file_id, name)
|
||||
successList.push({ file_id: targetPath, parent_file_id: normalizeWebDavPath(targetPath.split('/').slice(0, -1).join('/')), name, isDir: true })
|
||||
} catch (error) {
|
||||
console.error('WebDAV 重命名失败:', error)
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
const successList: { file_id: string; parent_file_id: string; name: string; isDir: boolean }[] = []
|
||||
const pathList = AliFileCmd.ResolveBaiduPaths(file_idList)
|
||||
for (let i = 0, maxi = file_idList.length; i < maxi; i++) {
|
||||
const file_id = file_idList[i]
|
||||
const path = pathList[i] || file_id
|
||||
const name = names[i] || ''
|
||||
if (!path || !name) continue
|
||||
const resp = await apiBaiduRename(user_id, path, name)
|
||||
if (resp.length) {
|
||||
successList.push({ file_id, name, parent_file_id: '', isDir: true })
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
const successList: { file_id: string; parent_file_id: string; name: string; isDir: boolean }[] = []
|
||||
for (let i = 0, maxi = file_idList.length; i < maxi; i++) {
|
||||
const file_id = file_idList[i]
|
||||
const name = names[i] || ''
|
||||
if (!file_id || !name) continue
|
||||
const resp = await apiDrive115Rename(user_id, file_id, name)
|
||||
if (resp.success) {
|
||||
successList.push({ file_id, name: resp.name, parent_file_id: '', isDir: true })
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
const batchList = ApiBatchMaker2('/file/update', file_idList, names, (file_id: string, name: string) => {
|
||||
return { drive_id: drive_id, file_id: file_id, name: name, check_name_mode: 'refuse' }
|
||||
})
|
||||
@@ -88,6 +247,13 @@ export default class AliFileCmd {
|
||||
|
||||
|
||||
static async ApiTrashCleanBatch(user_id: string, drive_id: string, ismessage: boolean, file_idList: string[]): Promise<string[]> {
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
message.error("暂不支持彻底删除,请移步至官方客户端操作")
|
||||
return []
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
return apiDrive115TrashDelete(user_id, file_idList)
|
||||
}
|
||||
const batchList = ApiBatchMaker('/file/delete', file_idList, (file_id: string) => {
|
||||
return { drive_id: drive_id, file_id: file_id }
|
||||
})
|
||||
@@ -96,6 +262,12 @@ export default class AliFileCmd {
|
||||
|
||||
|
||||
static async ApiTrashRestoreBatch(user_id: string, drive_id: string, ismessage: boolean, file_idList: string[]): Promise<string[]> {
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
return apiCloud123RecoverBatch(user_id, file_idList)
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
return apiDrive115TrashRestore(user_id, file_idList)
|
||||
}
|
||||
const batchList = ApiBatchMaker('/recyclebin/restore', file_idList, (file_id: string) => {
|
||||
return { drive_id: drive_id, file_id: file_id }
|
||||
})
|
||||
@@ -120,6 +292,10 @@ export default class AliFileCmd {
|
||||
}
|
||||
|
||||
static async ApiFileColorBatch(user_id: string, drive_id: string, description: string, color: string, file_idList: string[]) {
|
||||
if (isWebDavDrive(drive_id)) return
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') return
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') return
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') return
|
||||
// 防止加密标记清空
|
||||
let parts = description.split(',') || []
|
||||
let encryptPart = parts.find((part: any) => part.includes('xbyEncrypt')) || ''
|
||||
@@ -195,7 +371,41 @@ export default class AliFileCmd {
|
||||
}
|
||||
|
||||
|
||||
static async ApiMoveBatch(user_id: string, drive_id: string, file_idList: string[], to_drive_id: string, to_parent_file_id: string): Promise<string[]> {
|
||||
static async ApiMoveBatch(user_id: string, drive_id: string, file_idList: string[], to_drive_id: string, to_parent_file_id: string, to_parent_description: string = ''): Promise<string[]> {
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
if (drive_id !== to_drive_id) {
|
||||
message.error('WebDAV 暂不支持跨来源移动')
|
||||
return []
|
||||
}
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(drive_id))
|
||||
if (!connection) return []
|
||||
const targetParent = to_parent_file_id.includes('root') ? '/' : normalizeWebDavPath(to_parent_file_id)
|
||||
const successList: string[] = []
|
||||
for (const file_id of file_idList) {
|
||||
try {
|
||||
const targetPath = normalizeWebDavPath(`${targetParent}/${file_id.split('/').pop() || ''}`)
|
||||
await moveWebDavPath(connection, file_id, targetPath)
|
||||
successList.push(file_id)
|
||||
} catch (error) {
|
||||
console.error('WebDAV 移动失败:', error)
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '0'
|
||||
return apiCloud123MoveBatch(user_id, file_idList, to_parent_file_id)
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '0'
|
||||
return apiDrive115MoveBatch(user_id, file_idList, to_parent_file_id)
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '/'
|
||||
to_parent_file_id = AliFileCmd.ResolveBaiduTargetPath(to_parent_file_id, to_parent_file_id, to_parent_description)
|
||||
const paths = AliFileCmd.ResolveBaiduPaths(file_idList)
|
||||
return apiBaiduMove(user_id, paths, to_parent_file_id)
|
||||
}
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = 'root'
|
||||
const batchList = ApiBatchMaker('/file/move', file_idList, (file_id: string) => {
|
||||
if (drive_id == to_drive_id) return {
|
||||
@@ -216,7 +426,44 @@ export default class AliFileCmd {
|
||||
}
|
||||
|
||||
|
||||
static async ApiCopyBatch(user_id: string, drive_id: string, file_idList: string[], to_drive_id: string, to_parent_file_id: string): Promise<string[]> {
|
||||
static async ApiCopyBatch(user_id: string, drive_id: string, file_idList: string[], to_drive_id: string, to_parent_file_id: string, to_parent_description: string = ''): Promise<string[]> {
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
if (drive_id !== to_drive_id) {
|
||||
message.error('WebDAV 暂不支持跨来源复制')
|
||||
return []
|
||||
}
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(drive_id))
|
||||
if (!connection) return []
|
||||
const targetParent = to_parent_file_id.includes('root') ? '/' : normalizeWebDavPath(to_parent_file_id)
|
||||
const successList: string[] = []
|
||||
for (const file_id of file_idList) {
|
||||
try {
|
||||
const targetPath = normalizeWebDavPath(`${targetParent}/${file_id.split('/').pop() || ''}`)
|
||||
await copyWebDavPath(connection, file_id, targetPath)
|
||||
successList.push(file_id)
|
||||
} catch (error) {
|
||||
console.error('WebDAV 复制失败:', error)
|
||||
}
|
||||
}
|
||||
return successList
|
||||
}
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '0'
|
||||
if (file_idList.length <= 1) {
|
||||
return apiCloud123CopySingle(user_id, file_idList[0], to_parent_file_id)
|
||||
}
|
||||
return apiCloud123CopyBatch(user_id, file_idList, to_parent_file_id)
|
||||
}
|
||||
if (isDrive115User(user_id) || drive_id === 'drive115') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '0'
|
||||
return apiDrive115CopyBatch(user_id, file_idList, to_parent_file_id)
|
||||
}
|
||||
if (isBaiduUser(user_id) || drive_id === 'baidu') {
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = '/'
|
||||
to_parent_file_id = AliFileCmd.ResolveBaiduTargetPath(to_parent_file_id, to_parent_file_id, to_parent_description)
|
||||
const paths = AliFileCmd.ResolveBaiduPaths(file_idList)
|
||||
return apiBaiduCopy(user_id, paths, to_parent_file_id)
|
||||
}
|
||||
if (to_parent_file_id.includes('root')) to_parent_file_id = 'root'
|
||||
const batchList = ApiBatchMaker('/file/copy', file_idList, (file_id: string) => {
|
||||
if (drive_id == to_drive_id) return {
|
||||
@@ -238,6 +485,15 @@ export default class AliFileCmd {
|
||||
|
||||
|
||||
static async ApiGetFileBatch(user_id: string, drive_id: string, file_idList: string[]): Promise<IAliGetFileModel[]> {
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
if (file_idList.length === 1) {
|
||||
const detail = await apiCloud123FileDetail(user_id, file_idList[0])
|
||||
if (!detail) return []
|
||||
return [mapCloud123InfoToAliModel(detail)]
|
||||
}
|
||||
const list = await apiCloud123FileInfos(user_id, file_idList)
|
||||
return list.map((item) => mapCloud123InfoToAliModel(item))
|
||||
}
|
||||
const batchList = ApiBatchMaker('/file/get', file_idList, (file_id: string) => {
|
||||
return {
|
||||
drive_id: drive_id,
|
||||
|
||||
@@ -3,11 +3,12 @@ import { humanDateTime, humanDateTimeDateStr, humanExpiration, humanSize } from
|
||||
import message from '../utils/message'
|
||||
import AliHttp, { IUrlRespData } from './alihttp'
|
||||
import ServerHttp from './server'
|
||||
import { ApiBatch, ApiBatchMaker, ApiBatchSuccess } from './utils'
|
||||
import { useSettingStore } from '../store'
|
||||
import { ApiBatch, ApiBatchMaker, ApiBatchSuccess, isCloud123User } from './utils'
|
||||
import { useSettingStore, useMyShareStore } from '../store'
|
||||
import { IAliFileItem, IAliShareAnonymous, IAliShareBottleFish, IAliShareFileItem, IAliShareItem } from './alimodels'
|
||||
import getFileIcon from './fileicon'
|
||||
import { IAliBatchResult } from './models'
|
||||
import { apiCloud123ShareCreate, apiCloud123ShareUpdate } from '../cloud123/share'
|
||||
|
||||
export interface IAliShareFileResp {
|
||||
items: IAliShareFileItem[]
|
||||
@@ -246,6 +247,41 @@ export default class AliShare {
|
||||
|
||||
static async ApiCreatShare(user_id: string, drive_id: string, expiration: string, share_pwd: string, share_name: string, file_id_list: string[]): Promise<string | IAliShareItem> {
|
||||
if (!user_id || !drive_id || file_id_list.length == 0) return '创建分享链接失败数据错误'
|
||||
if (isCloud123User(user_id) || drive_id === 'cloud123') {
|
||||
const shareExpire = AliShare.toCloud123ShareExpire(expiration)
|
||||
const result = await apiCloud123ShareCreate(user_id, share_name, shareExpire, file_id_list, share_pwd)
|
||||
if (result.error) return result.error
|
||||
const shareUrl = result.shareKey ? `https://www.123pan.com/s/${result.shareKey}` : ''
|
||||
const fallbackExpiration = shareExpire > 0 ? new Date(Date.now() + shareExpire * 24 * 60 * 60 * 1000).toISOString() : ''
|
||||
const item: IAliShareItem = {
|
||||
created_at: '',
|
||||
creator: '',
|
||||
description: '',
|
||||
display_name: '',
|
||||
display_label: '',
|
||||
download_count: 0,
|
||||
drive_id: drive_id || 'cloud123',
|
||||
expiration: expiration || fallbackExpiration,
|
||||
expired: false,
|
||||
file_id: '',
|
||||
file_id_list: file_id_list,
|
||||
icon: 'iconwenjian',
|
||||
preview_count: 0,
|
||||
save_count: 0,
|
||||
share_id: result.shareId,
|
||||
share_msg: '',
|
||||
full_share_msg: '',
|
||||
share_name: share_name || '分享链接',
|
||||
share_policy: '',
|
||||
share_pwd: share_pwd || '',
|
||||
share_url: shareUrl,
|
||||
status: '',
|
||||
updated_at: '',
|
||||
is_share_saved: false,
|
||||
share_saved: ''
|
||||
}
|
||||
return item
|
||||
}
|
||||
const url = 'adrive/v2/share_link/create'
|
||||
const postData = { drive_id, expiration, share_pwd, share_name, file_id_list }
|
||||
const resp = await AliHttp.Post(url, postData, user_id, '')
|
||||
@@ -294,6 +330,10 @@ export default class AliShare {
|
||||
|
||||
|
||||
static async ApiCancelShareBatch(user_id: string, share_idList: string[]): Promise<string[]> {
|
||||
if (isCloud123User(user_id)) {
|
||||
message.info('当前网盘类型不支持')
|
||||
return []
|
||||
}
|
||||
const batchList = ApiBatchMaker('/share_link/cancel', share_idList, (share_id: string) => {
|
||||
return { share_id: share_id }
|
||||
})
|
||||
@@ -303,6 +343,28 @@ export default class AliShare {
|
||||
|
||||
static async ApiUpdateShareBatch(user_id: string, share_idList: string[], expirationList: string[], share_pwdList: string[], share_nameList: string[] | undefined): Promise<UpdateShareModel[]> {
|
||||
if (!share_idList || share_idList.length == 0) return []
|
||||
if (isCloud123User(user_id)) {
|
||||
const update = await apiCloud123ShareUpdate(user_id, share_idList)
|
||||
if (!update.success) {
|
||||
message.error('修改分享链接失败 ' + (update.error || ''))
|
||||
return []
|
||||
}
|
||||
const shareNameMap = new Map<string, string>()
|
||||
const existing = useMyShareStore().ListDataRaw
|
||||
for (let i = 0, maxi = existing.length; i < maxi; i++) {
|
||||
shareNameMap.set(existing[i].share_id, existing[i].share_name)
|
||||
}
|
||||
const successList: UpdateShareModel[] = []
|
||||
for (let i = 0, maxi = share_idList.length; i < maxi; i++) {
|
||||
successList.push({
|
||||
share_id: share_idList[i],
|
||||
share_pwd: share_pwdList[i] || '',
|
||||
expiration: expirationList[i] || '',
|
||||
share_name: share_nameList ? share_nameList[i] : (shareNameMap.get(share_idList[i]) || '')
|
||||
})
|
||||
}
|
||||
return successList
|
||||
}
|
||||
const batchList: string[] = []
|
||||
if (share_nameList) {
|
||||
for (let i = 0, maxi = share_idList.length; i < maxi; i++) {
|
||||
@@ -348,6 +410,18 @@ export default class AliShare {
|
||||
return successList
|
||||
}
|
||||
|
||||
private static toCloud123ShareExpire(expiration: string): number {
|
||||
if (!expiration) return 0
|
||||
const target = new Date(expiration).getTime()
|
||||
if (Number.isNaN(target)) return 0
|
||||
const diff = Math.max(0, target - Date.now())
|
||||
const days = Math.ceil(diff / (24 * 60 * 60 * 1000))
|
||||
if (days <= 1) return 1
|
||||
if (days <= 7) return 7
|
||||
if (days <= 30) return 30
|
||||
return 30
|
||||
}
|
||||
|
||||
|
||||
static async ApiSaveShareFilesBatch(share_id: string, share_token: string, user_id: string, drive_id: string, parent_file_id: string, file_idList: string[]): Promise<string> {
|
||||
if (!share_id || !share_token || !user_id || !drive_id || !parent_file_id) return 'error'
|
||||
|
||||
@@ -5,6 +5,8 @@ import AliHttp, { IUrlRespData } from './alihttp'
|
||||
import { IAliShareBottleFishItem, IAliShareItem, IAliShareRecentItem } from './alimodels'
|
||||
import AliDirFileList from './dirfilelist'
|
||||
import { useSettingStore } from '../store'
|
||||
import { isCloud123User } from './utils'
|
||||
import { apiCloud123ShareList } from '../cloud123/share'
|
||||
|
||||
export interface IAliShareResp {
|
||||
items: IAliShareItem[]
|
||||
@@ -37,6 +39,9 @@ export interface IAliShareBottleFishResp {
|
||||
export default class AliShareList {
|
||||
|
||||
static async ApiShareListAll(user_id: string): Promise<IAliShareResp> {
|
||||
if (isCloud123User(user_id)) {
|
||||
return await AliShareList.ApiCloud123ShareListAll(user_id)
|
||||
}
|
||||
const dir: IAliShareResp = {
|
||||
items: [],
|
||||
itemsKey: new Set(),
|
||||
@@ -55,6 +60,9 @@ export default class AliShareList {
|
||||
}
|
||||
|
||||
static async ApiShareListOnePage(dir: IAliShareResp): Promise<boolean> {
|
||||
if (isCloud123User(dir.m_user_id)) {
|
||||
return await AliShareList.ApiCloud123ShareListOnePage(dir)
|
||||
}
|
||||
const url = 'adrive/v3/share_link/list'
|
||||
const postData = {
|
||||
marker: dir.next_marker,
|
||||
@@ -263,6 +271,19 @@ export default class AliShareList {
|
||||
}
|
||||
|
||||
static async ApiShareListUntilShareID(user_id: string, share_id: string): Promise<boolean> {
|
||||
if (isCloud123User(user_id)) {
|
||||
let lastShareId = 0
|
||||
do {
|
||||
const resp = await apiCloud123ShareList(user_id, lastShareId, 100)
|
||||
if (resp.error) return false
|
||||
for (let i = 0, maxi = resp.list.length; i < maxi; i++) {
|
||||
if (String(resp.list[i].shareId) === share_id) return true
|
||||
}
|
||||
if (resp.lastShareId === -1) break
|
||||
lastShareId = resp.lastShareId
|
||||
} while (true)
|
||||
return false
|
||||
}
|
||||
const url = 'adrive/v3/share_link/list'
|
||||
const postData = {
|
||||
marker: '',
|
||||
@@ -278,4 +299,69 @@ export default class AliShareList {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private static async ApiCloud123ShareListAll(user_id: string): Promise<IAliShareResp> {
|
||||
const dir: IAliShareResp = {
|
||||
items: [],
|
||||
itemsKey: new Set(),
|
||||
next_marker: '',
|
||||
m_time: 0,
|
||||
m_user_id: user_id
|
||||
}
|
||||
do {
|
||||
const isGet = await AliShareList.ApiCloud123ShareListOnePage(dir)
|
||||
if (!isGet) break
|
||||
} while (dir.next_marker)
|
||||
return dir
|
||||
}
|
||||
|
||||
private static async ApiCloud123ShareListOnePage(dir: IAliShareResp): Promise<boolean> {
|
||||
const lastShareId = dir.next_marker ? Number(dir.next_marker) : 0
|
||||
const resp = await apiCloud123ShareList(dir.m_user_id, Number.isNaN(lastShareId) ? 0 : lastShareId, 100)
|
||||
if (resp.error) {
|
||||
message.warning('列出分享列表出错' + resp.error, 2)
|
||||
return false
|
||||
}
|
||||
const timeNow = new Date().getTime()
|
||||
for (let i = 0, maxi = resp.list.length; i < maxi; i++) {
|
||||
const item = resp.list[i]
|
||||
const share_id = String(item.shareId)
|
||||
if (dir.itemsKey.has(share_id)) continue
|
||||
const share_url = item.shareKey ? `https://www.123pan.com/s/${item.shareKey}` : ''
|
||||
const add: IAliShareItem = {
|
||||
created_at: '',
|
||||
creator: '',
|
||||
description: '',
|
||||
display_name: '',
|
||||
display_label: '',
|
||||
download_count: item.downloadCount || 0,
|
||||
drive_id: 'cloud123',
|
||||
expiration: item.expiration || '',
|
||||
expired: item.expired === 1,
|
||||
file_id: '',
|
||||
file_id_list: [],
|
||||
icon: 'iconwenjian',
|
||||
preview_count: item.previewCount || 0,
|
||||
save_count: item.saveCount || 0,
|
||||
share_id: share_id,
|
||||
share_msg: '',
|
||||
full_share_msg: '',
|
||||
share_name: item.shareName || '分享链接',
|
||||
share_policy: '',
|
||||
share_pwd: item.sharePwd || '',
|
||||
share_url: share_url,
|
||||
status: item.expired === 1 ? 'expired' : '',
|
||||
updated_at: '',
|
||||
is_share_saved: false,
|
||||
share_saved: ''
|
||||
}
|
||||
add.share_msg = humanExpiration(add.expiration, timeNow)
|
||||
if (add.expired) add.share_msg = '过期失效'
|
||||
if (!add.share_name) add.share_name = 'share_name'
|
||||
dir.items.push(add)
|
||||
dir.itemsKey.add(add.share_id)
|
||||
}
|
||||
dir.next_marker = resp.lastShareId === -1 ? '' : String(resp.lastShareId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,11 @@ export default class AliUploadDisk {
|
||||
return filePosMap.get(UploadID) || 0
|
||||
}
|
||||
|
||||
static RecordUploadProgress(UploadID: number, delta: number, pos: number): void {
|
||||
if (delta > 0) UploadSpeedTotal += delta
|
||||
filePosMap.set(UploadID, pos)
|
||||
}
|
||||
|
||||
|
||||
static DelFileUploadSpeed(UploadID: number): void {
|
||||
filePosMap.delete(UploadID)
|
||||
|
||||
@@ -17,9 +17,10 @@ export default class AliUploadMem {
|
||||
buff = Buffer.from(context, 'utf-8')
|
||||
if (encType) {
|
||||
let flowEnc = getFlowEnc(user_id, buff.length, encType)
|
||||
buff = flowEnc && flowEnc.encryptBuff(buff) || buff
|
||||
const encryptedBuff = flowEnc?.encryptBuff(buff)
|
||||
buff = encryptedBuff ? Buffer.from(encryptedBuff) : buff
|
||||
}
|
||||
const dd = await AliUploadHashPool.GetBuffHashProof(token!.access_token, buff)
|
||||
const dd = await AliUploadHashPool.GetBuffHashProof(token!.access_token, buff as Buffer)
|
||||
hash = dd.sha1
|
||||
proof = dd.proof_code
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import AliHttp from './alihttp'
|
||||
import message from '../utils/message'
|
||||
import DebugLog from '../utils/debuglog'
|
||||
import { IAliUserDriveCapacity, IAliUserDriveDetails } from './models'
|
||||
import { GetSignature } from './utils'
|
||||
import { GetSignature, isCloud123User } from './utils'
|
||||
import getUuid from 'uuid-by-string'
|
||||
import { useSettingStore } from '../store'
|
||||
import { refreshCloud123AccessToken } from '../utils/cloud123'
|
||||
|
||||
export const TokenReTimeMap = new Map<string, number>()
|
||||
export const TokenLockMap = new Map<string, number>()
|
||||
@@ -60,6 +61,7 @@ export default class AliUser {
|
||||
}
|
||||
|
||||
static async ApiTokenRefreshAccount(token: ITokenInfo, showMessage: boolean, forceRefresh: boolean = false): Promise<boolean> {
|
||||
if (token.user_id?.startsWith('cloud123_')) { return true }
|
||||
if (!token.refresh_token) return false
|
||||
if (!forceRefresh && new Date(token.expire_time).getTime() >= Date.now()) return true
|
||||
if (forceRefresh) {
|
||||
@@ -84,7 +86,7 @@ export default class AliUser {
|
||||
TokenLockMap.delete(token.user_id)
|
||||
if (AliHttp.IsSuccess(resp.code)) {
|
||||
TokenReTimeMap.set(resp.body.user_id, Date.now())
|
||||
token.tokenfrom = 'account'
|
||||
token.tokenfrom = 'aliyun'
|
||||
token.access_token = resp.body.access_token
|
||||
token.refresh_token = resp.body.refresh_token
|
||||
token.expires_in = resp.body.expires_in
|
||||
@@ -127,6 +129,30 @@ export default class AliUser {
|
||||
|
||||
|
||||
static async OpenApiTokenRefreshAccount(token: ITokenInfo, showMessage: boolean, forceRefresh: boolean = false): Promise<boolean> {
|
||||
if (isCloud123User(token)) {
|
||||
if (!token.refresh_token) return false
|
||||
if (!forceRefresh && new Date(token.expire_time).getTime() >= Date.now()) return true
|
||||
const refreshed = await refreshCloud123AccessToken(token.refresh_token)
|
||||
if (refreshed) {
|
||||
// 保留用户标识
|
||||
refreshed.user_id = token.user_id || refreshed.user_id
|
||||
refreshed.user_name = refreshed.user_name || token.user_name
|
||||
refreshed.nick_name = refreshed.nick_name || token.nick_name
|
||||
refreshed.avatar = refreshed.avatar || token.avatar
|
||||
refreshed.tokenfrom = 'cloud123'
|
||||
UserDAL.SaveUserToken(refreshed)
|
||||
await AliUser.Drive123UserInfo(refreshed)
|
||||
window.WebUserToken({
|
||||
user_id: refreshed.user_id,
|
||||
name: refreshed.user_name,
|
||||
access_token: refreshed.access_token,
|
||||
refresh: true
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (showMessage) message.error('刷新账号[' + token.user_name + '] token 失败,需要重新登录')
|
||||
return false
|
||||
}
|
||||
if (!token.open_api_refresh_token) return false
|
||||
if (!forceRefresh && new Date(token.open_api_expires_in).getTime() >= Date.now()) return true
|
||||
// 防止重复刷新
|
||||
@@ -147,8 +173,8 @@ export default class AliUser {
|
||||
}
|
||||
let { uiEnableOpenApiType, uiOpenApiClientId, uiOpenApiClientSecret } = useSettingStore()
|
||||
let url = 'https://openapi.alipan.com/oauth/access_token'
|
||||
let client_id = 'df43e22f022d4c04b6e29964f3b8b46d'
|
||||
let client_secret = '63f06c3c5c5d4e1196e2c13e8588ae29'
|
||||
let client_id = import.meta.env.VITE_ALIYUN_APP_ID || ''
|
||||
let client_secret = import.meta.env.VITE_ALIYUN_APP_SECRET || ''
|
||||
if (uiEnableOpenApiType === 'custom') {
|
||||
client_id = uiOpenApiClientId
|
||||
client_secret = uiOpenApiClientSecret
|
||||
@@ -291,6 +317,132 @@ export default class AliUser {
|
||||
return false
|
||||
}
|
||||
|
||||
static async Drive123UserInfo(token: ITokenInfo): Promise<boolean> {
|
||||
if (!token.access_token) return false
|
||||
const url = 'https://open-api.123pan.com/api/v1/user/info'
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return false
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0 || !data?.data) return false
|
||||
const info = data.data
|
||||
token.user_name = info.nickname || token.user_name
|
||||
token.nick_name = info.nickname || token.nick_name
|
||||
token.avatar = info.headImage || token.avatar
|
||||
if (typeof info.spaceUsed === 'number') token.used_size = info.spaceUsed
|
||||
if (typeof info.spacePermanent === 'number') token.total_size = info.spacePermanent
|
||||
if (typeof info.spaceUsed === 'number' && typeof info.spacePermanent === 'number') {
|
||||
token.spaceinfo = humanSize(info.spaceUsed) + ' / ' + humanSize(info.spacePermanent)
|
||||
}
|
||||
const vipInfo = Array.isArray(info.vipInfo) ? info.vipInfo : []
|
||||
const vipCurrent = vipInfo[0]
|
||||
if (vipCurrent?.vipLabel) token.vipname = vipCurrent.vipLabel
|
||||
if (vipCurrent?.endTime) token.vipexpire = vipCurrent.endTime
|
||||
if (info.vip) token.vipIcon = token.vipIcon || ''
|
||||
UserDAL.SaveUserToken(token)
|
||||
return true
|
||||
} catch (err: any) {
|
||||
DebugLog.mSaveWarning('Drive123UserInfo err=' + (err?.message || ''), err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static async Drive115UserInfo(token: ITokenInfo): Promise<boolean> {
|
||||
if (!token.access_token) return false
|
||||
const url = 'https://proapi.115.com/open/user/info'
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return false
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0 || !data?.data) return false
|
||||
const info = data.data
|
||||
token.user_name = info.user_name || token.user_name
|
||||
token.nick_name = info.user_name || token.nick_name
|
||||
token.avatar = info.user_face_m || info.user_face_l || info.user_face_s || token.avatar
|
||||
const space = info.rt_space_info || {}
|
||||
const totalSize = space?.all_total?.size
|
||||
const usedSize = space?.all_use?.size
|
||||
if (typeof usedSize === 'number') token.used_size = usedSize
|
||||
if (typeof totalSize === 'number') token.total_size = totalSize
|
||||
if (typeof usedSize === 'number' && typeof totalSize === 'number') {
|
||||
token.spaceinfo = humanSize(usedSize) + ' / ' + humanSize(totalSize)
|
||||
} else if (space?.all_use?.size_format && space?.all_total?.size_format) {
|
||||
token.spaceinfo = `${space.all_use.size_format} / ${space.all_total.size_format}`
|
||||
}
|
||||
if (info?.vip_info?.level_name) token.vipname = info.vip_info.level_name
|
||||
if (info?.vip_info?.expire) token.vipexpire = String(info.vip_info.expire)
|
||||
UserDAL.SaveUserToken(token)
|
||||
return true
|
||||
} catch (err: any) {
|
||||
DebugLog.mSaveWarning('Drive115UserInfo err=' + (err?.message || ''), err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static async DriveBaiduUserInfo(token: ITokenInfo): Promise<boolean> {
|
||||
if (!token.access_token) return false
|
||||
const params = new URLSearchParams({
|
||||
method: 'uinfo',
|
||||
access_token: token.access_token,
|
||||
vip_version: 'v2'
|
||||
})
|
||||
const url = `https://pan.baidu.com/rest/2.0/xpan/nas?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return false
|
||||
const data = await resp.json()
|
||||
if (data?.errno !== 0) return false
|
||||
token.user_name = data.netdisk_name || data.baidu_name || token.user_name
|
||||
token.nick_name = data.netdisk_name || data.baidu_name || token.nick_name
|
||||
token.avatar = data.avatar_url || token.avatar
|
||||
if (data.vip_type === 2) token.vipname = 'SVIP'
|
||||
if (data.vip_type === 1) token.vipname = 'VIP'
|
||||
if (data.uk) token.user_id = `baidu_${data.uk}`
|
||||
const quotaParams = new URLSearchParams({
|
||||
access_token: token.access_token,
|
||||
checkfree: '1',
|
||||
checkexpire: '1'
|
||||
})
|
||||
const quotaUrl = `https://pan.baidu.com/api/quota?${quotaParams.toString()}`
|
||||
const quotaResp = await fetch(quotaUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (quotaResp.ok) {
|
||||
const quota = await quotaResp.json()
|
||||
if (quota?.errno === 0) {
|
||||
if (typeof quota.total === 'number') token.total_size = quota.total
|
||||
if (typeof quota.used === 'number') token.used_size = quota.used
|
||||
if (typeof quota.free === 'number') token.free_size = quota.free
|
||||
if (typeof quota.expire === 'boolean') token.space_expire = quota.expire
|
||||
if (typeof quota.total === 'number' && typeof quota.used === 'number') {
|
||||
token.spaceinfo = humanSize(quota.used) + ' / ' + humanSize(quota.total)
|
||||
}
|
||||
}
|
||||
}
|
||||
UserDAL.SaveUserToken(token)
|
||||
return true
|
||||
} catch (err: any) {
|
||||
DebugLog.mSaveWarning('DriveBaiduUserInfo err=' + (err?.message || ''), err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static async ApiUserDriveInfo(token: ITokenInfo): Promise<boolean> {
|
||||
if (!token.user_id) return false
|
||||
let url = ''
|
||||
|
||||
@@ -16,8 +16,18 @@ import mime from 'mime-types'
|
||||
import { getEncPassword, getEncType } from '../utils/proxyhelper'
|
||||
|
||||
export function GetDriveID(user_id: string, drive: string): string {
|
||||
if ((drive || '').startsWith('webdav:')) return drive
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (token) {
|
||||
if (isCloud123User(user_id)) {
|
||||
return token.default_drive_id || token.resource_drive_id || 'cloud123'
|
||||
}
|
||||
if (isDrive115User(user_id)) {
|
||||
return token.default_drive_id || 'drive115'
|
||||
}
|
||||
if (isBaiduUser(user_id)) {
|
||||
return token.default_drive_id || 'baidu'
|
||||
}
|
||||
if (drive.includes('backup')) {
|
||||
return token.backup_drive_id
|
||||
} else if (drive.includes('resource')) {
|
||||
@@ -32,8 +42,20 @@ export function GetDriveID(user_id: string, drive: string): string {
|
||||
}
|
||||
|
||||
export function GetDriveType(user_id: string, drive_id: string): any {
|
||||
if ((drive_id || '').startsWith('webdav:')) {
|
||||
return { title: 'WebDAV', name: 'webdav', key: '/' }
|
||||
}
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (token) {
|
||||
if (isCloud123User(user_id)) {
|
||||
return { title: '网盘文件', name: 'cloud', key: 'cloud_root' }
|
||||
}
|
||||
if (isDrive115User(user_id)) {
|
||||
return { title: '网盘文件', name: 'drive115', key: 'drive115_root' }
|
||||
}
|
||||
if (isBaiduUser(user_id)) {
|
||||
return { title: '网盘文件', name: 'baidu', key: 'baidu_root' }
|
||||
}
|
||||
switch (drive_id) {
|
||||
case token.backup_drive_id:
|
||||
return { title: '备份盘', name: 'backup', key: 'backup_root' }
|
||||
@@ -50,6 +72,43 @@ export function GetDriveType(user_id: string, drive_id: string): any {
|
||||
return { title: '备份盘', name: 'backup', key: 'backup_root' }
|
||||
}
|
||||
|
||||
function resolveUserTokenInfo(user: string | { user_id?: string; tokenfrom?: string }) {
|
||||
if (typeof user === 'string') {
|
||||
return {
|
||||
user_id: user,
|
||||
tokenfrom: UserDAL.GetUserToken(user)?.tokenfrom || ''
|
||||
}
|
||||
}
|
||||
return {
|
||||
user_id: user?.user_id || '',
|
||||
tokenfrom: user?.tokenfrom || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function isCloud123User(user: string | { user_id?: string; tokenfrom?: string }): boolean {
|
||||
const { user_id, tokenfrom } = resolveUserTokenInfo(user)
|
||||
if (tokenfrom === 'cloud123') return true
|
||||
return user_id.startsWith('cloud123_')
|
||||
}
|
||||
|
||||
export function isDrive115User(user: string | { user_id?: string; tokenfrom?: string }): boolean {
|
||||
const { user_id, tokenfrom } = resolveUserTokenInfo(user)
|
||||
if (tokenfrom === '115') return true
|
||||
return user_id.startsWith('115_')
|
||||
}
|
||||
|
||||
export function isAliyunUser(user: string | { user_id?: string; tokenfrom?: string }): boolean {
|
||||
const { user_id, tokenfrom } = resolveUserTokenInfo(user)
|
||||
if (tokenfrom === 'aliyun') return true
|
||||
return user_id.startsWith('aliyun_')
|
||||
}
|
||||
|
||||
export function isBaiduUser(user: string | { user_id?: string; tokenfrom?: string }): boolean {
|
||||
const { user_id, tokenfrom } = resolveUserTokenInfo(user)
|
||||
if (tokenfrom === 'baidu') return true
|
||||
return user_id.startsWith('baidu_')
|
||||
}
|
||||
|
||||
export function GetSignature(nonce: number, user_id: string, deviceId: string) {
|
||||
const toHex = (bytes: Uint8Array) => {
|
||||
const hashArray = Array.from(bytes) // convert buffer to byte array
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export const apiDrive115CopyBatch = async (
|
||||
user_id: string,
|
||||
file_id_list: string[],
|
||||
target_parent_id: string
|
||||
): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const fileIds = file_id_list.filter(Boolean)
|
||||
if (fileIds.length === 0) return []
|
||||
const body = new URLSearchParams()
|
||||
body.set('pid', target_parent_id)
|
||||
body.set('file_id', fileIds.join(','))
|
||||
body.set('nodupli', '0')
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/ufile/copy', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '复制失败')
|
||||
return []
|
||||
}
|
||||
return fileIds
|
||||
} catch (err: any) {
|
||||
message.error('复制失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import { humanDateTimeDateStr, humanSize } from '../utils/format'
|
||||
import { HanToPin } from '../utils/utils'
|
||||
import type { IAliGetFileModel } from '../aliapi/alimodels'
|
||||
import getFileIcon from '../aliapi/fileicon'
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type Drive115FileItem = {
|
||||
fid: string | number
|
||||
pid: string | number
|
||||
fc: string | number
|
||||
fn: string
|
||||
fs?: number
|
||||
upt?: number
|
||||
uet?: number
|
||||
uppt?: number
|
||||
ico?: string
|
||||
}
|
||||
|
||||
export type Drive115FileListResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
data?: Drive115FileItem[]
|
||||
}
|
||||
|
||||
const API_URL = 'https://proapi.115.com/open/ufile/files'
|
||||
const SEARCH_URL = 'https://proapi.115.com/open/ufile/search'
|
||||
|
||||
const toTime = (val?: number | string) => {
|
||||
if (val === undefined || val === null) return 0
|
||||
const n = Number(val)
|
||||
if (!Number.isFinite(n) || n <= 0) return 0
|
||||
return n < 1000000000000 ? n * 1000 : n
|
||||
}
|
||||
|
||||
export const apiDrive115FileList = async (
|
||||
user_id: string,
|
||||
cid: string | number,
|
||||
limit = 200,
|
||||
offset = 0,
|
||||
showDir = true
|
||||
): Promise<Drive115FileItem[]> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
if (dbToken) {
|
||||
token = dbToken
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录 115 网盘')
|
||||
return []
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
params.set('cid', String(cid))
|
||||
params.set('limit', String(limit))
|
||||
params.set('offset', String(offset))
|
||||
params.set('cur', '1')
|
||||
params.set('show_dir', showDir ? '1' : '0')
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取 115 网盘文件列表失败')
|
||||
return []
|
||||
}
|
||||
const data = (await resp.json()) as Drive115FileListResp
|
||||
if (data?.code !== 0 || !Array.isArray(data.data)) {
|
||||
if (data?.message) message.error(data.message)
|
||||
else message.error('获取 115 网盘文件列表失败')
|
||||
return []
|
||||
}
|
||||
return data.data
|
||||
}
|
||||
|
||||
export const mapDrive115FileToAliModel = (item: Drive115FileItem, drive_id: string): IAliGetFileModel => {
|
||||
const isDir = String(item.fc) === '0'
|
||||
const name = item.fn || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const time = toTime(item.upt || item.uet || item.uppt)
|
||||
const timeStr = time ? humanDateTimeDateStr(new Date(time).toISOString()) : ''
|
||||
const size = Number(item.fs || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id,
|
||||
file_id: String(item.fid),
|
||||
parent_file_id: String(item.pid),
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export type Drive115SearchItem = {
|
||||
file_id: string
|
||||
parent_id: string
|
||||
file_name: string
|
||||
file_size?: string | number
|
||||
user_utime?: string | number
|
||||
file_category?: string
|
||||
ico?: string
|
||||
}
|
||||
|
||||
export type Drive115SearchResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
count?: number
|
||||
data?: Drive115SearchItem[]
|
||||
}
|
||||
|
||||
export type Drive115TrashItem = {
|
||||
id: string
|
||||
file_name: string
|
||||
type: string
|
||||
file_size?: string | number
|
||||
dtime?: string | number
|
||||
cid?: number
|
||||
parent_name?: string
|
||||
pick_code?: string
|
||||
}
|
||||
|
||||
export const apiDrive115Search = async (
|
||||
user_id: string,
|
||||
searchValue: string,
|
||||
limit = 200,
|
||||
offset = 0
|
||||
): Promise<{ items: Drive115SearchItem[]; total: number }> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token.access_token) {
|
||||
message.error('未登录 115 网盘')
|
||||
return { items: [], total: 0 }
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
params.set('search_value', searchValue)
|
||||
params.set('limit', String(limit))
|
||||
params.set('offset', String(offset))
|
||||
const url = `${SEARCH_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取 115 网盘搜索结果失败')
|
||||
return { items: [], total: 0 }
|
||||
}
|
||||
const data = (await resp.json()) as Drive115SearchResp
|
||||
if (data?.code !== 0 || !Array.isArray(data.data)) return { items: [], total: 0 }
|
||||
return { items: data.data, total: Number(data.count || data.data.length) }
|
||||
}
|
||||
|
||||
export const mapDrive115SearchToAliModel = (item: Drive115SearchItem, drive_id: string): IAliGetFileModel => {
|
||||
const isDir = String(item.file_category) !== '1'
|
||||
const name = item.file_name || ''
|
||||
const ext = isDir ? '' : (item.ico || name.split('.').pop() || '')
|
||||
const time = toTime(item.user_utime)
|
||||
const timeStr = time ? humanDateTimeDateStr(new Date(time).toISOString()) : ''
|
||||
const size = Number(item.file_size || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id,
|
||||
file_id: String(item.file_id),
|
||||
parent_file_id: String(item.parent_id),
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export const mapDrive115TrashToAliModel = (item: Drive115TrashItem, drive_id: string): IAliGetFileModel => {
|
||||
const isDir = String(item.type) === '2'
|
||||
const name = item.file_name || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const time = toTime(item.dtime)
|
||||
const timeStr = time ? humanDateTimeDateStr(new Date(time).toISOString()) : ''
|
||||
const size = Number(item.file_size || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id,
|
||||
file_id: String(item.id),
|
||||
parent_file_id: String(item.cid || ''),
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export const mapDrive115DetailToAliModel = (detail: {
|
||||
file_id: string
|
||||
name: string
|
||||
size: number
|
||||
isDir: boolean
|
||||
updated_at: string
|
||||
created_at: string
|
||||
play_long?: number
|
||||
}, drive_id: string): IAliGetFileModel => {
|
||||
const isDir = detail.isDir
|
||||
const name = detail.name || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const timeStr = humanDateTimeDateStr(detail.updated_at || detail.created_at || '')
|
||||
const time = new Date(detail.updated_at || detail.created_at || '').getTime()
|
||||
const size = Number(detail.size || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id,
|
||||
file_id: String(detail.file_id),
|
||||
parent_file_id: '',
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
import Config from '../config'
|
||||
|
||||
export const apiDrive115DownUrl = async (
|
||||
user_id: string,
|
||||
pick_code: string
|
||||
): Promise<{ url: string; size: number } | string> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return '未登录 115 网盘'
|
||||
if (!pick_code) return '文件提取码缺失'
|
||||
const body = new URLSearchParams()
|
||||
body.set('pick_code', pick_code)
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/ufile/downurl', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': Config.downAgent || '',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return '获取下载地址失败'
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0 || !data?.data) return data?.message || '获取下载地址失败'
|
||||
const firstKey = Object.keys(data.data)[0]
|
||||
const info = firstKey ? data.data[firstKey] : null
|
||||
const url = info?.url?.url || ''
|
||||
if (!url) return '获取下载地址失败'
|
||||
return { url, size: Number(info?.file_size || 0) }
|
||||
} catch (err: any) {
|
||||
message.error('获取下载地址失败 ' + (err?.message || ''))
|
||||
return '获取下载地址失败'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type Drive115MkdirResult = {
|
||||
file_id: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Drive115DetailResult = {
|
||||
file_id: string
|
||||
name: string
|
||||
size: number
|
||||
folder_count: number
|
||||
file_count: number
|
||||
isDir: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
pick_code: string
|
||||
sha1: string
|
||||
play_long: number
|
||||
path: { file_id: string; file_name: string }[]
|
||||
}
|
||||
|
||||
const API_URL = 'https://proapi.115.com/open/folder/add'
|
||||
const DETAIL_URL = 'https://proapi.115.com/open/folder/get_info'
|
||||
|
||||
const toIsoTime = (value?: string | number) => {
|
||||
if (!value && value !== 0) return ''
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || num <= 0) return ''
|
||||
const ms = num < 1000000000000 ? num * 1000 : num
|
||||
return new Date(ms).toISOString()
|
||||
}
|
||||
|
||||
export const apiDrive115Mkdir = async (user_id: string, parent_id: string, name: string): Promise<Drive115MkdirResult> => {
|
||||
const result: Drive115MkdirResult = { file_id: '', error: '新建文件夹失败' }
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return result
|
||||
const body = new URLSearchParams()
|
||||
body.set('pid', parent_id)
|
||||
body.set('file_name', name)
|
||||
try {
|
||||
const resp = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0 && data?.data?.file_id) {
|
||||
return { file_id: String(data.data.file_id), error: '' }
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
message.error('新建文件夹 失败 ' + (err?.message || ''))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const apiDrive115FileDetail = async (user_id: string, file_id: string): Promise<Drive115DetailResult | null> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return null
|
||||
const params = new URLSearchParams({ file_id })
|
||||
const url = `${DETAIL_URL}?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0 || !data?.data) return null
|
||||
const info = data.data
|
||||
return {
|
||||
file_id: String(info.file_id || file_id),
|
||||
name: info.file_name || '',
|
||||
size: Number(info.size_byte || 0),
|
||||
folder_count: Number(info.folder_count || 0),
|
||||
file_count: Number(info.count || 0),
|
||||
isDir: String(info.file_category) !== '1',
|
||||
created_at: toIsoTime(info.ptime),
|
||||
updated_at: toIsoTime(info.utime),
|
||||
pick_code: info.pick_code || '',
|
||||
sha1: info.sha1 || '',
|
||||
play_long: Number(info.play_long || 0),
|
||||
path: Array.isArray(info.paths)
|
||||
? info.paths.map((p: any) => ({
|
||||
file_id: String(p.file_id || ''),
|
||||
file_name: String(p.file_name || '')
|
||||
}))
|
||||
: []
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error('获取文件详情失败 ' + (err?.message || ''))
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export const apiDrive115MoveBatch = async (
|
||||
user_id: string,
|
||||
file_id_list: string[],
|
||||
target_parent_id: string
|
||||
): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const fileIds = file_id_list.filter(Boolean)
|
||||
if (fileIds.length === 0) return []
|
||||
const body = new URLSearchParams()
|
||||
body.set('file_ids', fileIds.join(','))
|
||||
body.set('to_cid', target_parent_id)
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/ufile/move', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '移动失败')
|
||||
return []
|
||||
}
|
||||
return fileIds
|
||||
} catch (err: any) {
|
||||
message.error('移动失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import crypto from 'crypto'
|
||||
import nodehttps from 'https'
|
||||
import type { ClientRequest } from 'http'
|
||||
|
||||
export type OssCredentials = {
|
||||
endpoint: string
|
||||
accessKeyId: string
|
||||
accessKeySecret: string
|
||||
securityToken: string
|
||||
}
|
||||
|
||||
const normalizeEndpoint = (endpoint: string) => {
|
||||
if (!endpoint) return ''
|
||||
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) return endpoint
|
||||
return `https://${endpoint}`
|
||||
}
|
||||
|
||||
const buildHostInfo = (endpoint: string, bucket: string) => {
|
||||
const url = new URL(normalizeEndpoint(endpoint))
|
||||
const host = url.hostname.startsWith(`${bucket}.`) ? url.hostname : `${bucket}.${url.hostname}`
|
||||
return { protocol: url.protocol, host }
|
||||
}
|
||||
|
||||
const hmacSha1 = (key: string, data: string) => crypto.createHmac('sha1', key).update(data).digest('base64')
|
||||
|
||||
const canonicalizeHeaders = (headers: Record<string, string>) => {
|
||||
const keys = Object.keys(headers)
|
||||
.map((k) => k.toLowerCase())
|
||||
.filter((k) => k.startsWith('x-oss-'))
|
||||
.sort()
|
||||
return keys.map((k) => `${k}:${headers[k] ?? headers[k.toLowerCase()]}`).join('\n') + (keys.length ? '\n' : '')
|
||||
}
|
||||
|
||||
const canonicalizeResource = (bucket: string, object: string, params?: Record<string, string | undefined>) => {
|
||||
let resource = `/${bucket}/${object}`
|
||||
if (params) {
|
||||
const keys = Object.keys(params).filter((k) => params[k] !== undefined).sort()
|
||||
if (keys.length) {
|
||||
const query = keys.map((k) => (params[k] ? `${k}=${params[k]}` : k)).join('&')
|
||||
resource += `?${query}`
|
||||
}
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
const buildAuthorization = (
|
||||
method: string,
|
||||
bucket: string,
|
||||
object: string,
|
||||
headers: Record<string, string>,
|
||||
params?: Record<string, string | undefined>
|
||||
) => {
|
||||
const contentMd5 = headers['Content-MD5'] || ''
|
||||
const contentType = headers['Content-Type'] || ''
|
||||
const date = headers['Date'] || ''
|
||||
const ossHeaders = canonicalizeHeaders(headers)
|
||||
const resource = canonicalizeResource(bucket, object, params)
|
||||
const stringToSign = `${method}\n${contentMd5}\n${contentType}\n${date}\n${ossHeaders}${resource}`
|
||||
return stringToSign
|
||||
}
|
||||
|
||||
export const ossInitiateMultipart = async (
|
||||
cred: OssCredentials,
|
||||
bucket: string,
|
||||
object: string,
|
||||
callback?: { callback?: string; callback_var?: string }
|
||||
) => {
|
||||
const { protocol, host } = buildHostInfo(cred.endpoint, bucket)
|
||||
const params = { uploads: '' }
|
||||
const headers: Record<string, string> = {
|
||||
Date: new Date().toUTCString(),
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
if (cred.securityToken) headers['x-oss-security-token'] = cred.securityToken
|
||||
if (callback?.callback) headers['x-oss-callback'] = callback.callback
|
||||
if (callback?.callback_var) headers['x-oss-callback-var'] = callback.callback_var
|
||||
const stringToSign = buildAuthorization('POST', bucket, object, headers, params)
|
||||
headers.Authorization = `OSS ${cred.accessKeyId}:${hmacSha1(cred.accessKeySecret, stringToSign)}`
|
||||
|
||||
const urlPath = `/${object}?uploads`
|
||||
const resp = await new Promise<{ status: number; body: string }>((resolve) => {
|
||||
const req: ClientRequest = nodehttps.request({
|
||||
method: 'POST',
|
||||
protocol,
|
||||
hostname: host,
|
||||
path: urlPath,
|
||||
headers
|
||||
}, (res) => {
|
||||
let raw = ''
|
||||
res.on('data', (chunk) => {
|
||||
raw += chunk
|
||||
})
|
||||
res.on('end', () => resolve({ status: res.statusCode || 0, body: raw }))
|
||||
})
|
||||
req.on('error', () => resolve({ status: 0, body: '' }))
|
||||
req.end()
|
||||
})
|
||||
return resp
|
||||
}
|
||||
|
||||
export const ossUploadPart = async (
|
||||
cred: OssCredentials,
|
||||
bucket: string,
|
||||
object: string,
|
||||
uploadId: string,
|
||||
partNumber: number,
|
||||
body: Buffer
|
||||
) => {
|
||||
const { protocol, host } = buildHostInfo(cred.endpoint, bucket)
|
||||
const params = { partNumber: String(partNumber), uploadId }
|
||||
const headers: Record<string, string> = {
|
||||
Date: new Date().toUTCString(),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': String(body.length)
|
||||
}
|
||||
if (cred.securityToken) headers['x-oss-security-token'] = cred.securityToken
|
||||
const stringToSign = buildAuthorization('PUT', bucket, object, headers, params)
|
||||
headers.Authorization = `OSS ${cred.accessKeyId}:${hmacSha1(cred.accessKeySecret, stringToSign)}`
|
||||
const urlPath = `/${object}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`
|
||||
const resp = await new Promise<{ status: number; etag: string }>((resolve) => {
|
||||
const req: ClientRequest = nodehttps.request({
|
||||
method: 'PUT',
|
||||
protocol,
|
||||
hostname: host,
|
||||
path: urlPath,
|
||||
headers
|
||||
}, (res) => {
|
||||
res.on('data', () => {})
|
||||
res.on('end', () => {
|
||||
const etag = (res.headers.etag as string) || ''
|
||||
resolve({ status: res.statusCode || 0, etag })
|
||||
})
|
||||
})
|
||||
req.on('error', () => resolve({ status: 0, etag: '' }))
|
||||
req.write(body)
|
||||
req.end()
|
||||
})
|
||||
return resp
|
||||
}
|
||||
|
||||
export const ossCompleteMultipart = async (
|
||||
cred: OssCredentials,
|
||||
bucket: string,
|
||||
object: string,
|
||||
uploadId: string,
|
||||
parts: { partNumber: number; etag: string }[],
|
||||
callback?: { callback?: string; callback_var?: string }
|
||||
) => {
|
||||
const { protocol, host } = buildHostInfo(cred.endpoint, bucket)
|
||||
const params = { uploadId }
|
||||
const bodyXml = ['<CompleteMultipartUpload>']
|
||||
parts.forEach((part) => {
|
||||
bodyXml.push('<Part>')
|
||||
bodyXml.push(`<PartNumber>${part.partNumber}</PartNumber>`)
|
||||
bodyXml.push(`<ETag>${part.etag}</ETag>`)
|
||||
bodyXml.push('</Part>')
|
||||
})
|
||||
bodyXml.push('</CompleteMultipartUpload>')
|
||||
const body = Buffer.from(bodyXml.join(''))
|
||||
const headers: Record<string, string> = {
|
||||
Date: new Date().toUTCString(),
|
||||
'Content-Type': 'application/xml',
|
||||
'Content-Length': String(body.length)
|
||||
}
|
||||
if (cred.securityToken) headers['x-oss-security-token'] = cred.securityToken
|
||||
if (callback?.callback) headers['x-oss-callback'] = callback.callback
|
||||
if (callback?.callback_var) headers['x-oss-callback-var'] = callback.callback_var
|
||||
const stringToSign = buildAuthorization('POST', bucket, object, headers, params)
|
||||
headers.Authorization = `OSS ${cred.accessKeyId}:${hmacSha1(cred.accessKeySecret, stringToSign)}`
|
||||
const urlPath = `/${object}?uploadId=${encodeURIComponent(uploadId)}`
|
||||
const resp = await new Promise<{ status: number; body: string }>((resolve) => {
|
||||
const req: ClientRequest = nodehttps.request({
|
||||
method: 'POST',
|
||||
protocol,
|
||||
hostname: host,
|
||||
path: urlPath,
|
||||
headers
|
||||
}, (res) => {
|
||||
let raw = ''
|
||||
res.on('data', (chunk) => {
|
||||
raw += chunk
|
||||
})
|
||||
res.on('end', () => resolve({ status: res.statusCode || 0, body: raw }))
|
||||
})
|
||||
req.on('error', () => resolve({ status: 0, body: '' }))
|
||||
req.write(body)
|
||||
req.end()
|
||||
})
|
||||
return resp
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type Drive115RenameResult = {
|
||||
file_id: string
|
||||
name: string
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export const apiDrive115Rename = async (
|
||||
user_id: string,
|
||||
file_id: string,
|
||||
new_name: string
|
||||
): Promise<Drive115RenameResult> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return { file_id, name: new_name, success: false }
|
||||
const body = new URLSearchParams()
|
||||
body.set('file_id', file_id)
|
||||
body.set('file_name', new_name)
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/ufile/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return { file_id, name: new_name, success: false }
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '重命名失败')
|
||||
return { file_id, name: new_name, success: false }
|
||||
}
|
||||
return { file_id, name: data?.data?.file_name || new_name, success: true }
|
||||
} catch (err: any) {
|
||||
message.error('重命名失败 ' + (err?.message || ''))
|
||||
return { file_id, name: new_name, success: false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export const apiDrive115TrashBatch = async (user_id: string, file_id_list: string[], parent_id?: string): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const ids = file_id_list.filter(Boolean)
|
||||
if (ids.length === 0) return []
|
||||
const body = new URLSearchParams()
|
||||
body.set('file_ids', ids.join(','))
|
||||
if (parent_id) body.set('parent_id', parent_id)
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/ufile/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '删除失败')
|
||||
return []
|
||||
}
|
||||
return ids
|
||||
} catch (err: any) {
|
||||
message.error('删除失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export type Drive115TrashItem = {
|
||||
id: string
|
||||
file_name: string
|
||||
type: string
|
||||
file_size?: string | number
|
||||
dtime?: string | number
|
||||
cid?: number
|
||||
parent_name?: string
|
||||
pick_code?: string
|
||||
}
|
||||
|
||||
export const apiDrive115TrashList = async (user_id: string, limit = 200, offset = 0): Promise<{ items: Drive115TrashItem[]; total: number }> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return { items: [], total: 0 }
|
||||
const params = new URLSearchParams()
|
||||
params.set('limit', String(limit))
|
||||
params.set('offset', String(offset))
|
||||
const url = `https://proapi.115.com/open/rb/list?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token.access_token}` } })
|
||||
if (!resp.ok) return { items: [], total: 0 }
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0 || !data?.data) return { items: [], total: 0 }
|
||||
const payload = data.data
|
||||
const items = Object.keys(payload)
|
||||
.filter((k) => !['offset', 'limit', 'count', 'rb_pass'].includes(k))
|
||||
.map((key) => payload[key] as Drive115TrashItem)
|
||||
return { items, total: Number(payload.count || items.length) }
|
||||
} catch (err: any) {
|
||||
message.error('获取回收站失败 ' + (err?.message || ''))
|
||||
return { items: [], total: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
export const apiDrive115TrashRestore = async (user_id: string, trash_ids: string[]): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const ids = trash_ids.filter(Boolean)
|
||||
if (ids.length === 0) return []
|
||||
const body = new URLSearchParams()
|
||||
body.set('tid', ids.join(','))
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/rb/revert', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '还原失败')
|
||||
return []
|
||||
}
|
||||
return ids
|
||||
} catch (err: any) {
|
||||
message.error('还原失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiDrive115TrashDelete = async (user_id: string, trash_ids?: string[]): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const body = new URLSearchParams()
|
||||
if (trash_ids && trash_ids.length > 0) {
|
||||
body.set('tid', trash_ids.join(','))
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('https://proapi.115.com/open/rb/del', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error(data?.message || '删除失败')
|
||||
return []
|
||||
}
|
||||
return trash_ids ? trash_ids : []
|
||||
} catch (err: any) {
|
||||
message.error('删除失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import { FileHandle } from 'fs/promises'
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
const API_BASE = 'https://proapi.115.com'
|
||||
|
||||
export type Drive115UploadInitData = {
|
||||
pick_code?: string
|
||||
status?: number
|
||||
sign_key?: string
|
||||
sign_check?: string
|
||||
file_id?: string
|
||||
target?: string
|
||||
bucket?: string
|
||||
object?: string
|
||||
callback?: string
|
||||
callback_var?: string
|
||||
}
|
||||
|
||||
export type Drive115UploadInitResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
data?: Drive115UploadInitData
|
||||
}
|
||||
|
||||
export type Drive115UploadTokenItem = {
|
||||
endpoint?: string
|
||||
AccessKeySecrett?: string
|
||||
AccessKeyId?: string
|
||||
SecurityToken?: string
|
||||
Expiration?: string
|
||||
}
|
||||
|
||||
export type Drive115UploadTokenResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
data?: Drive115UploadTokenItem[]
|
||||
}
|
||||
|
||||
const buildFormData = (fields: Record<string, string>) => {
|
||||
const boundary = '----xby115' + Date.now().toString(16) + Math.random().toString(16).slice(2)
|
||||
const chunks: Buffer[] = []
|
||||
const pushField = (name: string, value: string) => {
|
||||
chunks.push(Buffer.from(`--${boundary}\r\n`))
|
||||
chunks.push(Buffer.from(`Content-Disposition: form-data; name="${name}"\r\n\r\n`))
|
||||
chunks.push(Buffer.from(`${value}\r\n`))
|
||||
}
|
||||
Object.keys(fields).forEach((key) => {
|
||||
const val = fields[key]
|
||||
if (val !== undefined && val !== null && val !== '') pushField(key, String(val))
|
||||
})
|
||||
chunks.push(Buffer.from(`--${boundary}--\r\n`))
|
||||
return { body: Buffer.concat(chunks), boundary }
|
||||
}
|
||||
|
||||
const sha1Buffer = (buff: Buffer) => crypto.createHash('sha1').update(buff).digest('hex')
|
||||
|
||||
export const computeSha1 = async (fileHandle: FileHandle, size: number) => {
|
||||
const hash = crypto.createHash('sha1')
|
||||
const buff = Buffer.alloc(1024 * 1024)
|
||||
let offset = 0
|
||||
while (offset < size) {
|
||||
const read = await fileHandle.read(buff, 0, buff.length, offset)
|
||||
if (!read.bytesRead) break
|
||||
hash.update(buff.subarray(0, read.bytesRead))
|
||||
offset += read.bytesRead
|
||||
}
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export const computePreSha1 = async (fileHandle: FileHandle, size: number) => {
|
||||
const len = Math.min(128 * 1024, size)
|
||||
if (len <= 0) return sha1Buffer(Buffer.alloc(0))
|
||||
const buff = Buffer.alloc(len)
|
||||
const read = await fileHandle.read(buff, 0, len, 0)
|
||||
return sha1Buffer(buff.subarray(0, read.bytesRead))
|
||||
}
|
||||
|
||||
export const computeRangeSha1 = async (fileHandle: FileHandle, start: number, end: number) => {
|
||||
const size = end - start + 1
|
||||
const buff = Buffer.alloc(size)
|
||||
const read = await fileHandle.read(buff, 0, size, start)
|
||||
return sha1Buffer(buff.subarray(0, read.bytesRead))
|
||||
}
|
||||
|
||||
export const build115Target = (parentId: string | number) => {
|
||||
const id = parentId === undefined || parentId === null || parentId === '' ? 0 : Number(parentId)
|
||||
return `U_1_${Number.isFinite(id) ? id : 0}`
|
||||
}
|
||||
|
||||
export const apiDrive115GetUploadToken = async (user_id: string): Promise<Drive115UploadTokenItem[] | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录 115 网盘')
|
||||
return null
|
||||
}
|
||||
const url = `${API_BASE}/open/upload/get_token`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as Drive115UploadTokenResp
|
||||
if (!data?.state || data?.code !== 0 || !Array.isArray(data.data)) return null
|
||||
return data.data
|
||||
}
|
||||
|
||||
export const apiDrive115UploadInit = async (
|
||||
user_id: string,
|
||||
fileName: string,
|
||||
fileSize: number,
|
||||
target: string,
|
||||
fileSha1: string,
|
||||
preSha1: string,
|
||||
pickCode: string = '',
|
||||
topupload: string = '0',
|
||||
signKey: string = '',
|
||||
signVal: string = ''
|
||||
): Promise<Drive115UploadInitResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录 115 网盘')
|
||||
return null
|
||||
}
|
||||
const url = `${API_BASE}/open/upload/init`
|
||||
const fields: Record<string, string> = {
|
||||
file_name: path.basename(fileName),
|
||||
file_size: String(fileSize),
|
||||
target,
|
||||
fileid: fileSha1
|
||||
}
|
||||
if (preSha1) fields.preid = preSha1
|
||||
if (pickCode) fields.pick_code = pickCode
|
||||
if (topupload !== '') fields.topupload = topupload
|
||||
if (signKey) fields.sign_key = signKey
|
||||
if (signVal) fields.sign_val = signVal
|
||||
const { body, boundary } = buildFormData(fields)
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as Drive115UploadInitResp
|
||||
return data
|
||||
}
|
||||
|
||||
export const apiDrive115UploadResume = async (
|
||||
user_id: string,
|
||||
fileSize: number,
|
||||
target: string,
|
||||
fileSha1: string,
|
||||
pickCode: string
|
||||
): Promise<Drive115UploadInitResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录 115 网盘')
|
||||
return null
|
||||
}
|
||||
const url = `${API_BASE}/open/upload/resume`
|
||||
const fields: Record<string, string> = {
|
||||
file_size: String(fileSize),
|
||||
target,
|
||||
fileid: fileSha1,
|
||||
pick_code: pickCode
|
||||
}
|
||||
const { body, boundary } = buildFormData(fields)
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as Drive115UploadInitResp
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import path from 'path'
|
||||
import { OpenFileHandle } from '../utils/filehelper'
|
||||
import { IUploadingUI } from '../utils/dbupload'
|
||||
import AliUploadDisk from '../aliapi/uploaddisk'
|
||||
import { Sleep } from '../utils/format'
|
||||
import {
|
||||
apiDrive115GetUploadToken,
|
||||
apiDrive115UploadInit,
|
||||
apiDrive115UploadResume,
|
||||
build115Target,
|
||||
computePreSha1,
|
||||
computeRangeSha1,
|
||||
computeSha1
|
||||
} from './upload'
|
||||
import { apiDrive115FileList } from './dirfilelist'
|
||||
import { apiDrive115TrashBatch } from './trash'
|
||||
import { ossCompleteMultipart, ossInitiateMultipart, ossUploadPart } from './oss'
|
||||
|
||||
const PART_SIZE = 8 * 1024 * 1024
|
||||
|
||||
const parseSignCheck = (signCheck: string) => {
|
||||
const seg = signCheck.split('-')
|
||||
if (seg.length !== 2) return null
|
||||
const start = Number(seg[0])
|
||||
const end = Number(seg[1])
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
const findConflictName = async (user_id: string, parentId: string | number, name: string) => {
|
||||
const targetId = parentId === '' || parentId === undefined || parentId === null ? 0 : Number(parentId)
|
||||
const limit = 200
|
||||
let offset = 0
|
||||
while (offset < 2000) {
|
||||
const list = await apiDrive115FileList(user_id, targetId, limit, offset, true)
|
||||
if (!list.length) break
|
||||
const hit = list.find((item) => item.fn === name)
|
||||
if (hit) return { conflict: true, file_id: String(hit.fid) }
|
||||
if (list.length < limit) break
|
||||
offset += limit
|
||||
}
|
||||
return { conflict: false, file_id: '' }
|
||||
}
|
||||
|
||||
const ensureUploadName = async (user_id: string, parentId: string | number, originName: string, mode: string) => {
|
||||
const conflict = await findConflictName(user_id, parentId, originName)
|
||||
if (!conflict.conflict) return { name: originName, error: '' }
|
||||
|
||||
if (mode === 'refuse') {
|
||||
return { name: originName, error: '同名文件已存在' }
|
||||
}
|
||||
|
||||
if (mode === 'overwrite' || mode === 'ignore') {
|
||||
if (conflict.file_id) {
|
||||
await apiDrive115TrashBatch(user_id, [conflict.file_id], String(parentId || 0))
|
||||
}
|
||||
return { name: originName, error: '' }
|
||||
}
|
||||
|
||||
if (mode === 'auto_rename') {
|
||||
const extIndex = originName.lastIndexOf('.')
|
||||
const base = extIndex > 0 ? originName.slice(0, extIndex) : originName
|
||||
const ext = extIndex > 0 ? originName.slice(extIndex) : ''
|
||||
let index = 1
|
||||
while (index < 1000) {
|
||||
const candidate = `${base} (${index})${ext}`
|
||||
const candidateConflict = await findConflictName(user_id, parentId, candidate)
|
||||
if (!candidateConflict.conflict) return { name: candidate, error: '' }
|
||||
index += 1
|
||||
}
|
||||
return { name: originName, error: '自动重命名失败' }
|
||||
}
|
||||
|
||||
return { name: originName, error: '' }
|
||||
}
|
||||
|
||||
export default class Drive115UploadDisk {
|
||||
static async UploadOneFile(fileui: IUploadingUI): Promise<string> {
|
||||
const filePath = path.join(fileui.localFilePath, fileui.File.partPath)
|
||||
const handle = await OpenFileHandle(filePath)
|
||||
if (handle.error || !handle.handle) return handle.error || '打开文件失败'
|
||||
|
||||
fileui.Info.uploadState = 'hashing'
|
||||
const fileSha1 = await computeSha1(handle.handle, fileui.File.size)
|
||||
const preSha1 = await computePreSha1(handle.handle, fileui.File.size)
|
||||
fileui.Info.uploadState = 'running'
|
||||
await handle.handle.close()
|
||||
|
||||
if (!fileSha1) return '计算 sha1 失败'
|
||||
|
||||
const rename = await ensureUploadName(fileui.user_id, fileui.parent_file_id || 0, fileui.File.name, fileui.check_name_mode)
|
||||
if (rename.error) return rename.error
|
||||
const target = build115Target(fileui.parent_file_id || 0)
|
||||
let initResp = null
|
||||
if (fileui.Info.up_upload_id) {
|
||||
initResp = await apiDrive115UploadResume(fileui.user_id, fileui.File.size, target, fileSha1, fileui.Info.up_upload_id)
|
||||
}
|
||||
if (!initResp) {
|
||||
initResp = await apiDrive115UploadInit(
|
||||
fileui.user_id,
|
||||
rename.name,
|
||||
fileui.File.size,
|
||||
target,
|
||||
fileSha1,
|
||||
preSha1
|
||||
)
|
||||
}
|
||||
if (!initResp || !initResp.data) return '上传初始化失败'
|
||||
|
||||
if (initResp.data.sign_key && initResp.data.sign_check) {
|
||||
const range = parseSignCheck(initResp.data.sign_check)
|
||||
if (!range) return '签名验证失败'
|
||||
const rangeHandle = await OpenFileHandle(filePath)
|
||||
if (rangeHandle.error || !rangeHandle.handle) return rangeHandle.error || '打开文件失败'
|
||||
const rangeSha1 = await computeRangeSha1(rangeHandle.handle, range.start, range.end)
|
||||
await rangeHandle.handle.close()
|
||||
const signVal = rangeSha1.toUpperCase()
|
||||
initResp = await apiDrive115UploadInit(
|
||||
fileui.user_id,
|
||||
rename.name,
|
||||
fileui.File.size,
|
||||
target,
|
||||
fileSha1,
|
||||
preSha1,
|
||||
'',
|
||||
'0',
|
||||
initResp.data.sign_key,
|
||||
signVal
|
||||
)
|
||||
if (!initResp || !initResp.data) return '上传认证失败'
|
||||
}
|
||||
|
||||
const data = initResp.data
|
||||
if (data.status === 2) {
|
||||
fileui.File.uploaded_file_id = data.file_id || ''
|
||||
fileui.File.uploaded_is_rapid = true
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (!data.pick_code) return '上传初始化失败'
|
||||
fileui.Info.up_upload_id = data.pick_code
|
||||
|
||||
const tokenList = await apiDrive115GetUploadToken(fileui.user_id)
|
||||
if (!tokenList || tokenList.length === 0) return '获取上传凭证失败'
|
||||
const token = tokenList[0]
|
||||
if (!token.endpoint || !token.AccessKeyId || !token.AccessKeySecrett || !token.SecurityToken) {
|
||||
return '上传凭证信息不完整'
|
||||
}
|
||||
if (!data.bucket || !data.object) return '上传初始化信息不完整'
|
||||
|
||||
const init = await ossInitiateMultipart(
|
||||
{
|
||||
endpoint: token.endpoint,
|
||||
accessKeyId: token.AccessKeyId,
|
||||
accessKeySecret: token.AccessKeySecrett,
|
||||
securityToken: token.SecurityToken
|
||||
},
|
||||
data.bucket,
|
||||
data.object,
|
||||
{ callback: data.callback, callback_var: data.callback_var }
|
||||
)
|
||||
if (init.status !== 200) return 'OSS 初始化失败'
|
||||
|
||||
const uploadIdMatch = init.body.match(/<UploadId>(.+)<\/UploadId>/i)
|
||||
if (!uploadIdMatch) return 'OSS 初始化失败'
|
||||
const uploadId = uploadIdMatch[1]
|
||||
|
||||
const parts: { partNumber: number; etag: string }[] = []
|
||||
const partHandle = await OpenFileHandle(filePath)
|
||||
if (partHandle.error || !partHandle.handle) return partHandle.error || '打开文件失败'
|
||||
let offset = 0
|
||||
let partNumber = 1
|
||||
while (offset < fileui.File.size) {
|
||||
if (!fileui.IsRunning) {
|
||||
await partHandle.handle.close()
|
||||
return '已暂停'
|
||||
}
|
||||
const size = Math.min(PART_SIZE, fileui.File.size - offset)
|
||||
const buff = Buffer.alloc(size)
|
||||
const read = await partHandle.handle.read(buff, 0, size, offset)
|
||||
const body = buff.subarray(0, read.bytesRead)
|
||||
let ok = false
|
||||
let etag = ''
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const resp = await ossUploadPart(
|
||||
{
|
||||
endpoint: token.endpoint,
|
||||
accessKeyId: token.AccessKeyId,
|
||||
accessKeySecret: token.AccessKeySecrett,
|
||||
securityToken: token.SecurityToken
|
||||
},
|
||||
data.bucket,
|
||||
data.object,
|
||||
uploadId,
|
||||
partNumber,
|
||||
body
|
||||
)
|
||||
if (resp.status === 200 && resp.etag) {
|
||||
ok = true
|
||||
etag = resp.etag.replace(/\"/g, '')
|
||||
break
|
||||
}
|
||||
await Sleep(800)
|
||||
}
|
||||
if (!ok) {
|
||||
await partHandle.handle.close()
|
||||
return '分片上传失败'
|
||||
}
|
||||
parts.push({ partNumber, etag })
|
||||
offset += size
|
||||
AliUploadDisk.RecordUploadProgress(fileui.UploadID, size, offset)
|
||||
partNumber += 1
|
||||
}
|
||||
await partHandle.handle.close()
|
||||
|
||||
const complete = await ossCompleteMultipart(
|
||||
{
|
||||
endpoint: token.endpoint,
|
||||
accessKeyId: token.AccessKeyId,
|
||||
accessKeySecret: token.AccessKeySecrett,
|
||||
securityToken: token.SecurityToken
|
||||
},
|
||||
data.bucket,
|
||||
data.object,
|
||||
uploadId,
|
||||
parts,
|
||||
{ callback: data.callback, callback_var: data.callback_var }
|
||||
)
|
||||
if (complete.status !== 200) return 'OSS 合并失败'
|
||||
|
||||
fileui.File.uploaded_file_id = data.file_id || ''
|
||||
fileui.File.uploaded_is_rapid = false
|
||||
return 'success'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
import { apiDrive115FileDetail } from './filecmd'
|
||||
|
||||
type Drive115VideoUrlItem = {
|
||||
url: string
|
||||
height?: number
|
||||
width?: number
|
||||
definition?: number | string
|
||||
definition_n?: number | string
|
||||
title?: string
|
||||
}
|
||||
|
||||
type Drive115VideoPlayData = {
|
||||
file_id?: string
|
||||
file_name?: string
|
||||
play_long?: number | string
|
||||
user_def?: number
|
||||
multitrack_list?: { title?: string; is_selected?: string | number }[]
|
||||
definition_list_new?: number[]
|
||||
video_url?: Drive115VideoUrlItem[]
|
||||
}
|
||||
|
||||
type Drive115VideoPlayResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
data?: Drive115VideoPlayData
|
||||
}
|
||||
|
||||
type Drive115VideoHistoryResp = {
|
||||
state: boolean
|
||||
code: number
|
||||
message: string
|
||||
data?: { time?: string | number }[]
|
||||
}
|
||||
|
||||
const PLAY_URL = 'https://proapi.115.com/open/video/play'
|
||||
const HISTORY_URL = 'https://proapi.115.com/open/video/history'
|
||||
|
||||
const pickCodeCache = new Map<string, { pick_code: string; play_long: number }>()
|
||||
|
||||
export const apiDrive115VideoPlay = async (user_id: string, pick_code: string): Promise<Drive115VideoPlayData | string> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token || !pick_code) return '参数错误'
|
||||
const params = new URLSearchParams({ pick_code })
|
||||
const url = `${PLAY_URL}?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token.access_token}` }
|
||||
})
|
||||
if (!resp.ok) return '获取播放地址失败'
|
||||
const data = (await resp.json()) as Drive115VideoPlayResp
|
||||
if (data?.code !== 0 || !data?.data) {
|
||||
return data?.message || '获取播放地址失败'
|
||||
}
|
||||
return data.data
|
||||
} catch (err: any) {
|
||||
message.error('获取播放地址失败 ' + (err?.message || ''))
|
||||
return '获取播放地址失败'
|
||||
}
|
||||
}
|
||||
|
||||
export const apiDrive115VideoHistory = async (user_id: string, pick_code: string): Promise<number> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token || !pick_code) return 0
|
||||
const params = new URLSearchParams({ pick_code })
|
||||
const url = `${HISTORY_URL}?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token.access_token}` }
|
||||
})
|
||||
if (!resp.ok) return 0
|
||||
const data = (await resp.json()) as Drive115VideoHistoryResp
|
||||
if (data?.code !== 0 || !Array.isArray(data.data) || data.data.length === 0) return 0
|
||||
const time = Number(data.data[0]?.time || 0)
|
||||
return Number.isFinite(time) ? time : 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export const apiDrive115VideoHistoryUpdate = async (
|
||||
user_id: string,
|
||||
pick_code: string,
|
||||
time: number,
|
||||
watch_end: number = 0
|
||||
): Promise<boolean> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token || !pick_code) return false
|
||||
const body = new URLSearchParams()
|
||||
body.set('pick_code', pick_code)
|
||||
body.set('time', Math.max(0, Math.trunc(time)).toString())
|
||||
body.set('watch_end', watch_end ? '1' : '0')
|
||||
try {
|
||||
const resp = await fetch(HISTORY_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return false
|
||||
const data = await resp.json()
|
||||
return data?.code === 0 && data?.state === true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const getDrive115PickCode = async (user_id: string, file_id: string): Promise<{ pick_code: string; play_long: number } | null> => {
|
||||
const cacheKey = `${user_id}:${file_id}`
|
||||
if (pickCodeCache.has(cacheKey)) return pickCodeCache.get(cacheKey) || null
|
||||
const detail = await apiDrive115FileDetail(user_id, file_id)
|
||||
if (!detail?.pick_code) return null
|
||||
const meta = { pick_code: detail.pick_code, play_long: Number(detail.play_long || 0) }
|
||||
pickCodeCache.set(cacheKey, meta)
|
||||
return meta
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { humanDateTimeDateStr, humanSize } from '../utils/format'
|
||||
import { HanToPin } from '../utils/utils'
|
||||
import type { IAliGetFileModel } from '../aliapi/alimodels'
|
||||
import getFileIcon from '../aliapi/fileicon'
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type Cloud123FileItem = {
|
||||
fileId: number
|
||||
filename: string
|
||||
parentFileId: number
|
||||
type: number
|
||||
size: number
|
||||
category: number
|
||||
status: number
|
||||
trashed: number
|
||||
createAt?: string
|
||||
updateAt?: string
|
||||
}
|
||||
|
||||
export type Cloud123FileListResp = {
|
||||
code: number
|
||||
message: string
|
||||
data?: {
|
||||
lastFileId: number
|
||||
fileList: Cloud123FileItem[]
|
||||
}
|
||||
}
|
||||
|
||||
const API_URL = 'https://open-api.123pan.com/api/v2/file/list'
|
||||
|
||||
export const apiCloud123FileList = async (
|
||||
user_id: string,
|
||||
parentFileId: string | number,
|
||||
limit = 100,
|
||||
trashed: boolean = false,
|
||||
searchData: string = '',
|
||||
searchMode: number = 0
|
||||
): Promise<Cloud123FileItem[]> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录 123 网盘')
|
||||
return []
|
||||
}
|
||||
const params = new URLSearchParams()
|
||||
params.set('parentFileId', String(parentFileId))
|
||||
params.set('limit', String(limit))
|
||||
params.set('trashed', trashed ? '1' : '0')
|
||||
if (searchData) {
|
||||
params.set('parentFileId', '0')
|
||||
params.set('searchData', searchData)
|
||||
params.set('searchMode', String(searchMode))
|
||||
}
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
Platform: 'open_platform'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取 123 网盘文件列表失败')
|
||||
return []
|
||||
}
|
||||
const data = (await resp.json()) as Cloud123FileListResp
|
||||
if (data.code !== 0 || !data.data?.fileList) return []
|
||||
if (trashed) return data.data.fileList.filter((item) => item.trashed === 1)
|
||||
return data.data.fileList.filter((item) => item.trashed !== 1)
|
||||
}
|
||||
|
||||
export const mapCloud123FileToAliModel = (item: Cloud123FileItem): IAliGetFileModel => {
|
||||
const isDir = item.type === 1
|
||||
const name = item.filename
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const timeStr = humanDateTimeDateStr(item.updateAt || item.createAt || '')
|
||||
const time = new Date(item.updateAt || item.createAt || '').getTime()
|
||||
const size = item.size || 0
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id: 'cloud123',
|
||||
file_id: String(item.fileId),
|
||||
parent_file_id: String(item.parentFileId),
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export const mapCloud123InfoToAliModel = (item: any): IAliGetFileModel => {
|
||||
const isDir = Number(item.type) === 1
|
||||
const name = item.filename || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const timeStr = humanDateTimeDateStr(item.updateAt || item.createAt || '')
|
||||
const time = new Date(item.updateAt || item.createAt || '').getTime()
|
||||
const size = Number(item.size || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id: 'cloud123',
|
||||
file_id: String(item.fileId || item.fileID || item.file_id || ''),
|
||||
parent_file_id: String(item.parentFileId || item.parentFileID || item.parent_file_id || ''),
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
import Config from '../config'
|
||||
|
||||
export type Cloud123MkdirResult = {
|
||||
file_id: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export const apiCloud123Mkdir = async (user_id: string, parent_id: string, name: string): Promise<Cloud123MkdirResult> => {
|
||||
const result: Cloud123MkdirResult = { file_id: '', error: '新建文件夹失败' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return result
|
||||
const url = 'https://open-api.123pan.com/upload/v1/file/mkdir'
|
||||
const body = JSON.stringify({ parentID: parent_id, name })
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) {
|
||||
const fileId = data?.data?.fileID || data?.data?.fileId || data?.data?.file_id || ''
|
||||
return { file_id: fileId ? String(fileId) : '', error: '' }
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
message.error('新建文件夹 失败 ' + (err?.message || ''))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export type Cloud123RenameResult = {
|
||||
file_id: string
|
||||
parent_file_id: string
|
||||
name: string
|
||||
isDir: boolean
|
||||
}
|
||||
|
||||
export const apiCloud123TrashBatch = async (user_id: string, file_id_list: string[]): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/trash'
|
||||
const fileIDs = file_id_list.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIDs.length === 0) return []
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ fileIDs })
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
return fileIDs.map((id) => String(id))
|
||||
} catch (err: any) {
|
||||
message.error('删除到回收站失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123RecoverBatch = async (user_id: string, file_id_list: string[]): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/recover'
|
||||
const fileIDs = file_id_list.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIDs.length === 0) return []
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ fileIDs })
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) {
|
||||
message.error('还原失败:' + (data?.message || ''))
|
||||
return []
|
||||
}
|
||||
const abnormal = Array.isArray(data?.data?.abnormalFileIDs) ? data.data.abnormalFileIDs : []
|
||||
const abnormalSet = new Set<number>(abnormal.map((id: any) => Number(id)))
|
||||
return fileIDs.filter((id) => !abnormalSet.has(id)).map((id) => String(id))
|
||||
} catch (err: any) {
|
||||
message.error('从回收站恢复失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123DeleteBatch = async (user_id: string, file_id_list: string[]): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/delete'
|
||||
const fileIDs = file_id_list.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIDs.length === 0) return []
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ fileIDs })
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
return fileIDs.map((id) => String(id))
|
||||
} catch (err: any) {
|
||||
message.error('彻底删除失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123TrashDeleteAll = async (user_id: string): Promise<boolean> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return false
|
||||
const url = `https://www.123pan.com/api/v1/file/trash_delete_all?t=${Date.now()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ event: 'recycleClear', RequestSource: null })
|
||||
})
|
||||
if (!resp.ok) return false
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return false
|
||||
return true
|
||||
} catch (err: any) {
|
||||
message.error('清空回收站失败 ' + (err?.message || ''))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123CopySingle = async (user_id: string, file_id: string, targetDirId: string): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/copy'
|
||||
const body = {
|
||||
fileId: Number(file_id),
|
||||
targetDirId: Number(targetDirId)
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
const targetFileId = data?.data?.targetFileId ?? data?.data?.targetFileID
|
||||
return targetFileId ? [String(targetFileId)] : []
|
||||
} catch (err: any) {
|
||||
message.error('复制失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123CopyBatch = async (user_id: string, file_id_list: string[], targetDirId: string): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/async/copy'
|
||||
const fileIds = file_id_list.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIds.length === 0) return []
|
||||
const body = {
|
||||
fileIds,
|
||||
targetDirId: Number(targetDirId)
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
return fileIds.map((id) => String(id))
|
||||
} catch (err: any) {
|
||||
message.error('批量复制失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123MoveBatch = async (user_id: string, file_id_list: string[], toParentFileId: string): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/move'
|
||||
const fileIDs = file_id_list.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIDs.length === 0) return []
|
||||
const body = {
|
||||
fileIDs,
|
||||
toParentFileID: Number(toParentFileId)
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
return fileIDs.map((id) => String(id))
|
||||
} catch (err: any) {
|
||||
message.error('移动失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123FileDetail = async (user_id: string, file_id: string): Promise<any | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return null
|
||||
const url = `https://open-api.123pan.com/api/v1/file/detail?fileID=${encodeURIComponent(file_id)}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': Config.downAgent || '',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return null
|
||||
return data?.data || null
|
||||
} catch (err: any) {
|
||||
message.error('获取文件详情失败 ' + (err?.message || ''))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123FileInfos = async (user_id: string, file_ids: string[]): Promise<any[]> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return []
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/infos'
|
||||
const fileIds = file_ids.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (fileIds.length === 0) return []
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ fileIds })
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return []
|
||||
const list = Array.isArray(data?.data?.fileList) ? data.data.fileList : []
|
||||
return list
|
||||
} catch (err: any) {
|
||||
message.error('获取文件详情失败 ' + (err?.message || ''))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123DownloadInfo = async (user_id: string, file_id: string): Promise<{ url: string } | string> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return '未登录 123 网盘'
|
||||
const url = `https://open-api.123pan.com/api/v1/file/download_info?fileId=${encodeURIComponent(file_id)}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取下载地址失败')
|
||||
return '获取下载地址失败'
|
||||
}
|
||||
const data = await resp.json()
|
||||
console.log('[cloud123] download_info', { file_id, code: data?.code, message: data?.message, hasUrl: !!data?.data?.downloadUrl })
|
||||
if (data?.code !== 0) {
|
||||
const msg = data?.message || '获取下载地址失败'
|
||||
message.error(msg)
|
||||
return msg
|
||||
}
|
||||
const downloadUrl = data?.data?.downloadUrl || ''
|
||||
if (!downloadUrl) {
|
||||
message.error('获取下载地址失败')
|
||||
return '获取下载地址失败'
|
||||
}
|
||||
return { url: downloadUrl }
|
||||
} catch (err: any) {
|
||||
message.error('获取下载地址失败 ' + (err?.message || ''))
|
||||
return err?.message || '获取下载地址失败'
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123Rename = async (user_id: string, file_id: string, newName: string): Promise<{ success: boolean; error: string }> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return { success: false, error: '未登录 123 网盘' }
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/name'
|
||||
const body = JSON.stringify({ fileId: Number(file_id), fileName: newName })
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return { success: false, error: '重命名失败' }
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) return { success: true, error: '' }
|
||||
return { success: false, error: data?.message || '重命名失败' }
|
||||
} catch (err: any) {
|
||||
message.error('重命名 失败 ' + (err?.message || ''))
|
||||
return { success: false, error: err?.message || '重命名失败' }
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCloud123RenameBatch = async (
|
||||
user_id: string,
|
||||
file_id_list: string[],
|
||||
name_list: string[]
|
||||
): Promise<Cloud123RenameResult[]> => {
|
||||
const result: Cloud123RenameResult[] = []
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return result
|
||||
const url = 'https://open-api.123pan.com/api/v1/file/rename'
|
||||
const renameList: string[] = []
|
||||
for (let i = 0; i < file_id_list.length; i++) {
|
||||
const file_id = file_id_list[i]
|
||||
const name = name_list[i]
|
||||
if (!file_id || !name) continue
|
||||
renameList.push(`${file_id}|${name}`)
|
||||
}
|
||||
if (renameList.length === 0) return result
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify({ renameList })
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return result
|
||||
for (let i = 0; i < renameList.length; i++) {
|
||||
const file_id = file_id_list[i]
|
||||
const name = name_list[i]
|
||||
result.push({ file_id, parent_file_id: '', name, isDir: false })
|
||||
}
|
||||
} catch (err: any) {
|
||||
message.error('批量重命名 失败 ' + (err?.message || ''))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
|
||||
export type Cloud123OfflineCreateResult = {
|
||||
taskId: number | null
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Cloud123OfflineProcessResult = {
|
||||
process: number
|
||||
status: number
|
||||
error: string
|
||||
}
|
||||
|
||||
export const apiCloud123OfflineCreate = async (user_id: string, url: string, fileName: string, dirID?: string) => {
|
||||
const result: Cloud123OfflineCreateResult = { taskId: null, error: '创建离线下载失败' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
result.error = '请先登录123云盘'
|
||||
return result
|
||||
}
|
||||
const body: any = { url }
|
||||
if (fileName) body.fileName = fileName
|
||||
if (dirID && !dirID.includes('root')) {
|
||||
const dirNum = Number(dirID)
|
||||
if (!Number.isNaN(dirNum)) body.dirID = dirNum
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('https://open-api.123pan.com/api/v1/offline/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) {
|
||||
const taskId = data?.data?.taskID ?? data?.data?.taskId
|
||||
return { taskId: typeof taskId === 'number' ? taskId : Number(taskId), error: '' }
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const apiCloud123OfflineProcess = async (user_id: string, taskID: string) => {
|
||||
const result: Cloud123OfflineProcessResult = { process: 0, status: 0, error: '' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
result.error = '请先登录123云盘'
|
||||
return result
|
||||
}
|
||||
const taskIdNum = Number(taskID)
|
||||
if (Number.isNaN(taskIdNum)) {
|
||||
result.error = '任务ID无效'
|
||||
return result
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`https://open-api.123pan.com/api/v1/offline/download/process?taskID=${taskIdNum}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
result.error = '获取离线下载进度失败'
|
||||
return result
|
||||
}
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) {
|
||||
result.process = Number(data?.data?.process) || 0
|
||||
result.status = Number(data?.data?.status) || 0
|
||||
return result
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
|
||||
export type Cloud123ShareCreateResult = {
|
||||
shareId: string
|
||||
shareKey: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Cloud123ShareListItem = {
|
||||
shareId: number
|
||||
shareKey: string
|
||||
shareName: string
|
||||
expiration: string
|
||||
expired: number
|
||||
sharePwd: string
|
||||
trafficSwitch: number
|
||||
trafficLimitSwitch: number
|
||||
trafficLimit: number
|
||||
bytesCharge: number
|
||||
previewCount: number
|
||||
downloadCount: number
|
||||
saveCount: number
|
||||
}
|
||||
|
||||
export type Cloud123ShareListResult = {
|
||||
list: Cloud123ShareListItem[]
|
||||
lastShareId: number
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Cloud123ShareUpdateResult = {
|
||||
success: boolean
|
||||
error: string
|
||||
}
|
||||
|
||||
export const apiCloud123ShareCreate = async (
|
||||
user_id: string,
|
||||
shareName: string,
|
||||
shareExpire: number,
|
||||
fileIDList: string[],
|
||||
sharePwd: string,
|
||||
trafficSwitch?: number,
|
||||
trafficLimitSwitch?: number,
|
||||
trafficLimit?: number
|
||||
): Promise<Cloud123ShareCreateResult> => {
|
||||
const result: Cloud123ShareCreateResult = { shareId: '', shareKey: '', error: '创建分享链接失败' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
result.error = '请先登录123云盘'
|
||||
return result
|
||||
}
|
||||
const url = 'https://open-api.123pan.com/api/v1/share/create'
|
||||
const fileIDListStr = fileIDList.join(',')
|
||||
const safeShareName = shareName || '分享链接'
|
||||
const body: any = {
|
||||
shareName: safeShareName,
|
||||
shareExpire,
|
||||
fileIDList: fileIDListStr
|
||||
}
|
||||
if (sharePwd) body.sharePwd = sharePwd
|
||||
if (trafficSwitch) body.trafficSwitch = trafficSwitch
|
||||
if (trafficLimitSwitch) body.trafficLimitSwitch = trafficLimitSwitch
|
||||
if (trafficLimit) body.trafficLimit = trafficLimit
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) {
|
||||
const shareId = data?.data?.shareID ?? data?.data?.shareId ?? ''
|
||||
const shareKey = data?.data?.shareKey ?? ''
|
||||
return { shareId: shareId ? String(shareId) : '', shareKey: shareKey || '', error: '' }
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const apiCloud123ShareList = async (
|
||||
user_id: string,
|
||||
lastShareId: number,
|
||||
limit: number
|
||||
): Promise<Cloud123ShareListResult> => {
|
||||
const result: Cloud123ShareListResult = { list: [], lastShareId: -1, error: '' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
result.error = '请先登录123云盘'
|
||||
return result
|
||||
}
|
||||
const url = `https://open-api.123pan.com/api/v1/share/list?limit=${limit}&lastShareId=${lastShareId}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
result.error = '获取分享列表失败'
|
||||
return result
|
||||
}
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) {
|
||||
result.list = Array.isArray(data?.data?.shareList) ? data.data.shareList : []
|
||||
result.lastShareId = typeof data?.data?.lastShareId === 'number' ? data.data.lastShareId : -1
|
||||
return result
|
||||
}
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const apiCloud123ShareUpdate = async (
|
||||
user_id: string,
|
||||
shareIdList: string[],
|
||||
trafficSwitch?: number,
|
||||
trafficLimitSwitch?: number,
|
||||
trafficLimit?: number
|
||||
): Promise<Cloud123ShareUpdateResult> => {
|
||||
const result: Cloud123ShareUpdateResult = { success: false, error: '修改分享链接失败' }
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
result.error = '请先登录123云盘'
|
||||
return result
|
||||
}
|
||||
const ids = shareIdList.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||
if (ids.length === 0) {
|
||||
result.error = '分享链接ID错误'
|
||||
return result
|
||||
}
|
||||
const url = 'https://open-api.123pan.com/api/v1/share/list/info'
|
||||
const body: any = {
|
||||
shareIdList: ids
|
||||
}
|
||||
if (trafficSwitch) body.trafficSwitch = trafficSwitch
|
||||
if (trafficLimitSwitch) body.trafficLimitSwitch = trafficLimitSwitch
|
||||
if (trafficLimit) body.trafficLimit = trafficLimit
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return result
|
||||
const data = await resp.json()
|
||||
if (data?.code === 0) return { success: true, error: '' }
|
||||
if (data?.message) result.error = data.message
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
|
||||
export type Cloud123CreateFileResp = {
|
||||
reuse: boolean
|
||||
fileID: number
|
||||
preuploadID: string
|
||||
sliceSize: number
|
||||
servers: string[]
|
||||
}
|
||||
|
||||
export const cloud123CreateFile = async (
|
||||
user_id: string,
|
||||
parentFileID: number,
|
||||
filename: string,
|
||||
etag: string,
|
||||
size: number,
|
||||
duplicate: number = 1,
|
||||
containDir: boolean = false
|
||||
): Promise<Cloud123CreateFileResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return null
|
||||
const url = 'https://open-api.123pan.com/upload/v2/file/create'
|
||||
const body = {
|
||||
parentFileID,
|
||||
filename,
|
||||
etag,
|
||||
size,
|
||||
duplicate,
|
||||
containDir
|
||||
}
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return null
|
||||
return {
|
||||
reuse: !!data?.data?.reuse,
|
||||
fileID: Number(data?.data?.fileID || 0),
|
||||
preuploadID: data?.data?.preuploadID || '',
|
||||
sliceSize: Number(data?.data?.sliceSize || 0),
|
||||
servers: Array.isArray(data?.data?.servers) ? data.data.servers : []
|
||||
}
|
||||
}
|
||||
|
||||
export const cloud123UploadComplete = async (
|
||||
server: string,
|
||||
accessToken: string,
|
||||
preuploadID: string
|
||||
): Promise<{ completed: boolean; fileID: number }> => {
|
||||
const url = normalizeServer(server) + '/upload/v2/file/upload_complete'
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
body: JSON.stringify({ preuploadID })
|
||||
})
|
||||
if (!resp.ok) return { completed: false, fileID: 0 }
|
||||
const data = await resp.json()
|
||||
if (data?.code !== 0) return { completed: false, fileID: 0 }
|
||||
return {
|
||||
completed: !!data?.data?.completed,
|
||||
fileID: Number(data?.data?.fileID || 0)
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeServer = (server: string): string => {
|
||||
if (!server) return ''
|
||||
if (server.startsWith('http://') || server.startsWith('https://')) return server
|
||||
return `https://${server}`
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import crypto from 'crypto'
|
||||
import nodehttps from 'https'
|
||||
import type { ClientRequest } from 'http'
|
||||
import path from 'path'
|
||||
import { FileHandle } from 'fs/promises'
|
||||
import { OpenFileHandle } from '../utils/filehelper'
|
||||
import UserDAL from '../user/userdal'
|
||||
import { IUploadingUI } from '../utils/dbupload'
|
||||
import { cloud123CreateFile, cloud123UploadComplete, normalizeServer } from './upload'
|
||||
import AliUploadDisk from '../aliapi/uploaddisk'
|
||||
import { Sleep } from '../utils/format'
|
||||
|
||||
const md5Buffer = (buff: Buffer) => crypto.createHash('md5').update(buff).digest('hex')
|
||||
|
||||
const md5File = async (filePath: string): Promise<string> => {
|
||||
const hash = crypto.createHash('md5')
|
||||
const fh = await OpenFileHandle(filePath)
|
||||
if (fh.error || !fh.handle) return ''
|
||||
const handle = fh.handle
|
||||
const buff = Buffer.alloc(1024 * 1024)
|
||||
let pos = 0
|
||||
while (true) {
|
||||
const read = await handle.read(buff, 0, buff.length, pos)
|
||||
if (!read.bytesRead) break
|
||||
hash.update(buff.subarray(0, read.bytesRead))
|
||||
pos += read.bytesRead
|
||||
}
|
||||
await handle.close()
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
const buildMultipart = (preuploadID: string, sliceNo: number, sliceMD5: string, sliceBuff: Buffer) => {
|
||||
const boundary = '----xby123pan' + Date.now().toString(16) + Math.random().toString(16).slice(2)
|
||||
const parts: Buffer[] = []
|
||||
const pushField = (name: string, value: string) => {
|
||||
parts.push(Buffer.from(`--${boundary}\r\n`))
|
||||
parts.push(Buffer.from(`Content-Disposition: form-data; name="${name}"\r\n\r\n`))
|
||||
parts.push(Buffer.from(`${value}\r\n`))
|
||||
}
|
||||
pushField('preuploadID', preuploadID)
|
||||
pushField('sliceNo', String(sliceNo))
|
||||
pushField('sliceMD5', sliceMD5)
|
||||
parts.push(Buffer.from(`--${boundary}\r\n`))
|
||||
parts.push(Buffer.from(`Content-Disposition: form-data; name="slice"; filename="slice"\r\n`))
|
||||
parts.push(Buffer.from(`Content-Type: application/octet-stream\r\n\r\n`))
|
||||
parts.push(sliceBuff)
|
||||
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`))
|
||||
const body = Buffer.concat(parts)
|
||||
return { body, boundary }
|
||||
}
|
||||
|
||||
const uploadSlice = (server: string, accessToken: string, preuploadID: string, sliceNo: number, sliceMD5: string, sliceBuff: Buffer): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const { body, boundary } = buildMultipart(preuploadID, sliceNo, sliceMD5, sliceBuff)
|
||||
const url = new URL(normalizeServer(server) + '/upload/v2/file/slice')
|
||||
const options = {
|
||||
method: 'POST',
|
||||
hostname: url.hostname,
|
||||
path: url.pathname,
|
||||
protocol: url.protocol,
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length,
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
const req: ClientRequest = nodehttps.request(options, (res) => {
|
||||
res.on('data', () => {})
|
||||
res.on('end', () => {
|
||||
resolve(res.statusCode === 200)
|
||||
})
|
||||
})
|
||||
req.on('error', () => resolve(false))
|
||||
req.write(body)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
const readSlice = async (fileHandle: FileHandle, start: number, size: number): Promise<Buffer> => {
|
||||
const buff = Buffer.alloc(size)
|
||||
const read = await fileHandle.read(buff, 0, size, start)
|
||||
return buff.subarray(0, read.bytesRead)
|
||||
}
|
||||
|
||||
export default class Cloud123UploadDisk {
|
||||
static async UploadOneFile(fileui: IUploadingUI): Promise<string> {
|
||||
const token = await UserDAL.GetUserTokenFromDB(fileui.user_id)
|
||||
if (!token?.access_token) return '找不到上传token,请重试'
|
||||
|
||||
const filePath = path.join(fileui.localFilePath, fileui.File.partPath)
|
||||
fileui.Info.uploadState = 'hashing'
|
||||
const etag = await md5File(filePath)
|
||||
fileui.Info.uploadState = 'running'
|
||||
if (!etag) return '计算md5失败'
|
||||
|
||||
const parentFileID = Number(fileui.parent_file_id || 0) || 0
|
||||
const createResp = await cloud123CreateFile(
|
||||
fileui.user_id,
|
||||
parentFileID,
|
||||
fileui.File.name,
|
||||
etag,
|
||||
fileui.File.size,
|
||||
1,
|
||||
false
|
||||
)
|
||||
if (!createResp) return '创建文件失败'
|
||||
if (createResp.reuse) {
|
||||
fileui.File.uploaded_file_id = String(createResp.fileID || '')
|
||||
fileui.File.uploaded_is_rapid = true
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (!createResp.preuploadID || !createResp.sliceSize || createResp.servers.length === 0) {
|
||||
return '创建文件返回信息不完整'
|
||||
}
|
||||
|
||||
const server = createResp.servers[0]
|
||||
const fileHandle = await OpenFileHandle(filePath)
|
||||
if (fileHandle.error || !fileHandle.handle) return fileHandle.error || '打开文件失败'
|
||||
|
||||
const sliceSize = createResp.sliceSize
|
||||
const total = fileui.File.size
|
||||
let offset = 0
|
||||
let sliceNo = 1
|
||||
while (offset < total) {
|
||||
if (!fileui.IsRunning) {
|
||||
await fileHandle.handle.close()
|
||||
return '已暂停'
|
||||
}
|
||||
const size = Math.min(sliceSize, total - offset)
|
||||
const buff = await readSlice(fileHandle.handle, offset, size)
|
||||
const sliceMD5 = md5Buffer(buff)
|
||||
let ok = false
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ok = await uploadSlice(server, token.access_token, createResp.preuploadID, sliceNo, sliceMD5, buff)
|
||||
if (ok) break
|
||||
await Sleep(800)
|
||||
}
|
||||
if (!ok) {
|
||||
await fileHandle.handle.close()
|
||||
return '分片上传失败'
|
||||
}
|
||||
offset += size
|
||||
AliUploadDisk.RecordUploadProgress(fileui.UploadID, size, offset)
|
||||
sliceNo += 1
|
||||
}
|
||||
await fileHandle.handle.close()
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const complete = await cloud123UploadComplete(server, token.access_token, createResp.preuploadID)
|
||||
if (complete.completed && complete.fileID) {
|
||||
fileui.File.uploaded_file_id = String(complete.fileID)
|
||||
fileui.File.uploaded_is_rapid = false
|
||||
return 'success'
|
||||
}
|
||||
await Sleep(1000)
|
||||
}
|
||||
return '上传完成确认超时'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type Cloud123TranscodeItem = {
|
||||
url: string
|
||||
resolution?: string
|
||||
duration?: number
|
||||
height?: number
|
||||
status?: number
|
||||
bitRate?: number
|
||||
progress?: number
|
||||
updateAt?: string
|
||||
}
|
||||
|
||||
export type Cloud123TranscodeResp = {
|
||||
code: number
|
||||
message: string
|
||||
data?: {
|
||||
list?: Cloud123TranscodeItem[]
|
||||
status?: number
|
||||
}
|
||||
}
|
||||
|
||||
const TRANSCODE_URL = 'https://open-api.123pan.com/api/v1/video/transcode/list'
|
||||
|
||||
export const apiCloud123TranscodeList = async (
|
||||
user_id: string,
|
||||
fileId: string
|
||||
): Promise<{ status: number; list: Cloud123TranscodeItem[] } | string> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) return '未登录 123 云盘'
|
||||
if (!fileId) return '参数错误'
|
||||
const params = new URLSearchParams({ fileId })
|
||||
const url = `${TRANSCODE_URL}?${params.toString()}`
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Platform: 'open_platform',
|
||||
Authorization: `Bearer ${token.access_token}`
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return '获取转码列表失败'
|
||||
const data = (await resp.json()) as Cloud123TranscodeResp
|
||||
if (data?.code !== 0) return data?.message || '获取转码列表失败'
|
||||
const list = Array.isArray(data?.data?.list) ? data.data!.list! : []
|
||||
const status = Number(data?.data?.status || 0)
|
||||
return { status, list }
|
||||
} catch (err: any) {
|
||||
message.error('获取转码列表失败 ' + (err?.message || ''))
|
||||
return '获取转码列表失败'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { IAliGetFileModel } from '../aliapi/alimodels'
|
||||
import getFileIcon from '../aliapi/fileicon'
|
||||
import { HanToPin } from '../utils/utils'
|
||||
import { humanDateTimeDateStr, humanSize } from '../utils/format'
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
export type BaiduFileItem = {
|
||||
fs_id: number
|
||||
path: string
|
||||
server_filename: string
|
||||
size: number
|
||||
server_mtime: number
|
||||
server_ctime: number
|
||||
local_mtime?: number
|
||||
local_ctime?: number
|
||||
isdir: number
|
||||
category?: number
|
||||
md5?: string
|
||||
dir_empty?: number
|
||||
thumbs?: { url1?: string; url2?: string; url3?: string }
|
||||
}
|
||||
|
||||
export type BaiduFileListResp = {
|
||||
errno: number
|
||||
list?: BaiduFileItem[]
|
||||
}
|
||||
|
||||
const API_URL = 'https://pan.baidu.com/rest/2.0/xpan/file'
|
||||
|
||||
export const apiBaiduFileList = async (
|
||||
user_id: string,
|
||||
dir: string,
|
||||
order = 'name',
|
||||
start = 0,
|
||||
limit = 1000
|
||||
): Promise<BaiduFileItem[]> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录百度网盘')
|
||||
return []
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
method: 'list',
|
||||
access_token: token.access_token,
|
||||
dir: dir || '/',
|
||||
order,
|
||||
start: String(start),
|
||||
limit: String(limit),
|
||||
web: '1',
|
||||
folder: '0',
|
||||
desc: '0'
|
||||
})
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取百度网盘文件列表失败')
|
||||
return []
|
||||
}
|
||||
const data = (await resp.json()) as BaiduFileListResp
|
||||
if (data?.errno !== 0 || !Array.isArray(data.list)) return []
|
||||
return data.list
|
||||
}
|
||||
|
||||
export const apiBaiduSearch = async (
|
||||
user_id: string,
|
||||
keyword: string,
|
||||
dir = '/',
|
||||
recursion = true
|
||||
): Promise<BaiduFileItem[]> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录百度网盘')
|
||||
return []
|
||||
}
|
||||
if (!keyword) return []
|
||||
const params = new URLSearchParams({
|
||||
method: 'search',
|
||||
access_token: token.access_token,
|
||||
key: keyword,
|
||||
dir: dir || '/',
|
||||
num: '500',
|
||||
web: '1'
|
||||
})
|
||||
if (recursion) params.set('recursion', '1')
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) {
|
||||
message.error('获取百度网盘搜索结果失败')
|
||||
return []
|
||||
}
|
||||
const data = (await resp.json()) as BaiduFileListResp & { has_more?: number }
|
||||
if (data?.errno !== 0 || !Array.isArray(data.list)) return []
|
||||
return data.list
|
||||
}
|
||||
|
||||
export const mapBaiduFileToAliModel = (item: BaiduFileItem, drive_id: string, parentDir: string): IAliGetFileModel => {
|
||||
const isDir = item.isdir === 1
|
||||
const name = item.server_filename || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const time = (item.server_mtime || item.server_ctime || 0) * 1000
|
||||
const timeStr = time ? humanDateTimeDateStr(new Date(time).toISOString()) : ''
|
||||
const size = Number(item.size || 0)
|
||||
let category = ''
|
||||
let icon = 'iconfile-folder'
|
||||
if (!isDir) {
|
||||
const iconInfo = getFileIcon('', ext, ext, '', size)
|
||||
category = iconInfo[0]
|
||||
icon = iconInfo[1]
|
||||
}
|
||||
const thumbnail = item.thumbs?.url2 || item.thumbs?.url1 || item.thumbs?.url3 || ''
|
||||
const description = item.fs_id ? `baidu_fsid:${item.fs_id};baidu_path:${item.path || ''}` : ''
|
||||
return {
|
||||
__v_skip: true,
|
||||
drive_id,
|
||||
file_id: String(item.fs_id) || '',
|
||||
parent_file_id: parentDir,
|
||||
name: name,
|
||||
namesearch: HanToPin(name),
|
||||
ext: ext,
|
||||
mime_type: '',
|
||||
mime_extension: ext,
|
||||
category,
|
||||
icon,
|
||||
file_count: 0,
|
||||
size,
|
||||
sizeStr: humanSize(size),
|
||||
time,
|
||||
timeStr,
|
||||
starred: false,
|
||||
isDir,
|
||||
thumbnail,
|
||||
path: item.path || '',
|
||||
description
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
import type { IAliFileItem } from '../aliapi/alimodels'
|
||||
|
||||
export type BaiduFileMetaItem = {
|
||||
category?: number
|
||||
dlink?: string
|
||||
filename?: string
|
||||
fs_id: number
|
||||
isdir?: number
|
||||
server_ctime?: number
|
||||
server_mtime?: number
|
||||
local_ctime?: number
|
||||
local_mtime?: number
|
||||
size?: number
|
||||
md5?: string
|
||||
path?: string
|
||||
duration?: number // 添加顶层 duration 字段
|
||||
thumbs?: {
|
||||
icon?: string
|
||||
url1?: string
|
||||
url2?: string
|
||||
url3?: string
|
||||
url4?: string // 添加 url4 字段
|
||||
}
|
||||
width?: number
|
||||
height?: number
|
||||
date_taken?: number
|
||||
orientation?: string
|
||||
oper_id?: number // 添加 oper_id 字段
|
||||
media_info?: {
|
||||
channels?: number
|
||||
duration?: number
|
||||
duration_ms?: number
|
||||
extra_info?: string
|
||||
file_size?: string
|
||||
frame_rate?: number
|
||||
height?: number
|
||||
width?: number
|
||||
meta_info?: string
|
||||
resolution?: string
|
||||
rotate?: number
|
||||
sample_rate?: number
|
||||
use_segment?: number
|
||||
}
|
||||
}
|
||||
|
||||
type BaiduFileMetaResp = {
|
||||
errno: number
|
||||
list?: BaiduFileMetaItem[]
|
||||
}
|
||||
|
||||
const API_URL = 'https://pan.baidu.com/rest/2.0/xpan/multimedia'
|
||||
|
||||
const mapCategory = (category?: number) => {
|
||||
switch (category) {
|
||||
case 1: return 'video'
|
||||
case 2: return 'audio'
|
||||
case 3: return 'image'
|
||||
case 4: return 'doc'
|
||||
case 5: return 'app'
|
||||
case 7: return 'bt'
|
||||
default: return 'other'
|
||||
}
|
||||
}
|
||||
|
||||
export const apiBaiduFileMetas = async (
|
||||
user_id: string,
|
||||
fsids: number[],
|
||||
needDlink = 0,
|
||||
needThumb = 1,
|
||||
needExtra = 1,
|
||||
needMedia = 1,
|
||||
needDetail = 1
|
||||
): Promise<BaiduFileMetaItem[] | null> => {
|
||||
let token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
const dbToken = await UserDAL.GetUserTokenFromDB(user_id)
|
||||
|
||||
if (dbToken) {
|
||||
|
||||
token = dbToken
|
||||
|
||||
}
|
||||
}
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录百度网盘')
|
||||
return null
|
||||
}
|
||||
if (!fsids.length) return null
|
||||
const params = new URLSearchParams({
|
||||
method: 'filemetas',
|
||||
access_token: token.access_token,
|
||||
fsids: JSON.stringify(fsids),
|
||||
dlink: String(needDlink),
|
||||
thumb: String(needThumb),
|
||||
extra: String(needExtra),
|
||||
needmedia: String(needMedia),
|
||||
detail: String(needDetail)
|
||||
})
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as BaiduFileMetaResp
|
||||
if (data?.errno !== 0 || !Array.isArray(data.list)) return null
|
||||
return data.list
|
||||
}
|
||||
|
||||
export const mapBaiduMetaToAliFileItem = (item: BaiduFileMetaItem, drive_id: string, file_id: string): IAliFileItem => {
|
||||
const isDir = item.isdir === 1
|
||||
const name = item.filename || ''
|
||||
const ext = isDir ? '' : (name.split('.').pop() || '')
|
||||
const createdAt = item.server_ctime ? new Date(item.server_ctime * 1000).toISOString() : ''
|
||||
const updatedAt = item.server_mtime ? new Date(item.server_mtime * 1000).toISOString() : ''
|
||||
const thumbnail = item.thumbs?.url2 || item.thumbs?.url1 || item.thumbs?.url3 || item.thumbs?.url4 || item.thumbs?.icon || ''
|
||||
const category = mapCategory(item.category)
|
||||
const parentPath = item.path ? item.path.substring(0, item.path.lastIndexOf('/')) || '/' : ''
|
||||
|
||||
// 优先使用 media_info 中的 duration,如果没有则使用顶层的 duration
|
||||
const duration = item.media_info?.duration || item.duration || 0
|
||||
|
||||
return {
|
||||
drive_id,
|
||||
domain_id: '',
|
||||
description: item.fs_id ? `baidu_fsid:${item.fs_id}${item.path ? `;baidu_path:${item.path}` : ''}` : '',
|
||||
file_id: String(item.fs_id || file_id),
|
||||
name,
|
||||
type: isDir ? 'folder' : 'file',
|
||||
content_type: '',
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
file_extension: ext,
|
||||
hidden: false,
|
||||
size: Number(item.size || 0),
|
||||
starred: false,
|
||||
status: '',
|
||||
upload_id: '',
|
||||
parent_file_id: parentPath,
|
||||
crc64_hash: '',
|
||||
content_hash: item.md5 || '',
|
||||
content_hash_name: 'md5',
|
||||
download_url: item.dlink || '',
|
||||
url: '',
|
||||
category,
|
||||
encrypt_mode: '',
|
||||
punish_flag: 0,
|
||||
thumbnail,
|
||||
mime_extension: ext,
|
||||
mime_type: '',
|
||||
play_cursor: '',
|
||||
duration: duration ? String(duration) : '',
|
||||
video_media_metadata: (item.media_info || duration || item.width || item.height)
|
||||
? {
|
||||
duration: duration,
|
||||
height: item.media_info?.height || item.height,
|
||||
width: item.media_info?.width || item.width
|
||||
}
|
||||
: undefined,
|
||||
image_media_metadata: item.width || item.height || item.date_taken
|
||||
? {
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
time: item.date_taken ? new Date(item.date_taken * 1000).toISOString() : undefined
|
||||
}
|
||||
: undefined,
|
||||
user_meta: ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import UserDAL from '../user/userdal'
|
||||
import message from '../utils/message'
|
||||
|
||||
type BaiduFileManagerResp = {
|
||||
errno: number
|
||||
info?: { errno: number; path?: string }[]
|
||||
taskid?: number
|
||||
}
|
||||
|
||||
const API_URL = 'https://pan.baidu.com/rest/2.0/xpan/file'
|
||||
|
||||
const requestFileManager = async (
|
||||
user_id: string,
|
||||
opera: 'copy' | 'move' | 'rename' | 'delete',
|
||||
filelist: any[],
|
||||
ondup = 'fail',
|
||||
asyncMode = 1
|
||||
): Promise<string[]> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) {
|
||||
message.error('未登录百度网盘')
|
||||
return []
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
method: 'filemanager',
|
||||
access_token: token.access_token,
|
||||
opera
|
||||
})
|
||||
const url = `${API_URL}?${params.toString()}`
|
||||
const body = new URLSearchParams()
|
||||
body.set('async', String(asyncMode))
|
||||
body.set('filelist', JSON.stringify(filelist))
|
||||
if (opera !== 'delete') body.set('ondup', ondup)
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body
|
||||
})
|
||||
if (!resp.ok) return []
|
||||
const data = (await resp.json()) as BaiduFileManagerResp
|
||||
if (data?.errno !== 0) return []
|
||||
return (data.info || []).map((item) => item.path || '').filter(Boolean)
|
||||
}
|
||||
|
||||
export const apiBaiduCopy = async (user_id: string, paths: string[], dest: string): Promise<string[]> => {
|
||||
const filelist = paths.map((path) => ({ path, dest }))
|
||||
return requestFileManager(user_id, 'copy', filelist, 'fail', 1)
|
||||
}
|
||||
|
||||
export const apiBaiduMove = async (user_id: string, paths: string[], dest: string): Promise<string[]> => {
|
||||
const filelist = paths.map((path) => ({ path, dest }))
|
||||
return requestFileManager(user_id, 'move', filelist, 'fail', 1)
|
||||
}
|
||||
|
||||
export const apiBaiduRename = async (user_id: string, path: string, newname: string): Promise<string[]> => {
|
||||
const filelist = [{ path, newname }]
|
||||
return requestFileManager(user_id, 'rename', filelist, 'overwrite', 1)
|
||||
}
|
||||
|
||||
export const apiBaiduDelete = async (user_id: string, paths: string[]): Promise<string[]> => {
|
||||
return requestFileManager(user_id, 'delete', paths, 'fail', 1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import crypto from 'crypto'
|
||||
import nodehttps from 'https'
|
||||
import type { ClientRequest } from 'http'
|
||||
import path from 'path'
|
||||
import { FileHandle } from 'fs/promises'
|
||||
import UserDAL from '../user/userdal'
|
||||
|
||||
const BAIDU_API_HOST = 'https://pan.baidu.com'
|
||||
const BAIDU_PCS_HOST = 'https://d.pcs.baidu.com'
|
||||
const BAIDU_PCS_APP_ID = import.meta.env.VITE_BAIDU_PCS_APP_ID || ''
|
||||
|
||||
const BAIDU_BLOCK_SIZE = 4 * 1024 * 1024
|
||||
const BAIDU_SLICE_SIZE = 256 * 1024
|
||||
|
||||
export type BaiduPrecreateResp = {
|
||||
errno: number
|
||||
return_type?: number
|
||||
uploadid?: string
|
||||
block_list?: string[]
|
||||
}
|
||||
|
||||
export type BaiduLocateUploadResp = {
|
||||
errno: number
|
||||
host?: string[]
|
||||
}
|
||||
|
||||
export type BaiduCreateResp = {
|
||||
errno: number
|
||||
fs_id?: number
|
||||
path?: string
|
||||
}
|
||||
|
||||
const md5Buffer = (buff: Buffer) => crypto.createHash('md5').update(buff).digest('hex')
|
||||
|
||||
const normalizePath = (parentPath: string, name: string) => {
|
||||
let base = parentPath || '/'
|
||||
if (base.includes('root')) base = '/'
|
||||
if (!base.startsWith('/')) base = '/' + base
|
||||
if (base !== '/' && base.endsWith('/')) base = base.slice(0, -1)
|
||||
return base === '/' ? `/${name}` : `${base}/${name}`
|
||||
}
|
||||
|
||||
export const buildBaiduUploadPath = (parentPath: string, name: string) => {
|
||||
const folder = parentPath || '/'
|
||||
return normalizePath(folder, name)
|
||||
}
|
||||
|
||||
export const computeBaiduBlockList = async (fileHandle: FileHandle, size: number) => {
|
||||
const blockList: string[] = []
|
||||
const fullHash = crypto.createHash('md5')
|
||||
let sliceMd5 = ''
|
||||
if (size === 0) {
|
||||
const empty = md5Buffer(Buffer.alloc(0))
|
||||
return {
|
||||
blockList: [empty],
|
||||
contentMd5: empty,
|
||||
sliceMd5: empty
|
||||
}
|
||||
}
|
||||
let offset = 0
|
||||
while (offset < size) {
|
||||
const chunkSize = Math.min(BAIDU_BLOCK_SIZE, size - offset)
|
||||
const buff = Buffer.alloc(chunkSize)
|
||||
const read = await fileHandle.read(buff, 0, chunkSize, offset)
|
||||
if (!read.bytesRead) break
|
||||
const realBuff = buff.subarray(0, read.bytesRead)
|
||||
if (!sliceMd5) {
|
||||
const sliceBuff = realBuff.subarray(0, Math.min(BAIDU_SLICE_SIZE, realBuff.length))
|
||||
sliceMd5 = md5Buffer(sliceBuff)
|
||||
}
|
||||
blockList.push(md5Buffer(realBuff))
|
||||
fullHash.update(realBuff)
|
||||
offset += read.bytesRead
|
||||
}
|
||||
return {
|
||||
blockList,
|
||||
contentMd5: fullHash.digest('hex'),
|
||||
sliceMd5: sliceMd5 || ''
|
||||
}
|
||||
}
|
||||
|
||||
export const apiBaiduPrecreate = async (
|
||||
user_id: string,
|
||||
filePath: string,
|
||||
size: number,
|
||||
blockList: string[],
|
||||
contentMd5: string,
|
||||
sliceMd5: string,
|
||||
rtype: number = 2
|
||||
): Promise<BaiduPrecreateResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return null
|
||||
const url = new URL('/rest/2.0/xpan/file', BAIDU_API_HOST)
|
||||
url.searchParams.set('method', 'precreate')
|
||||
url.searchParams.set('access_token', token.access_token)
|
||||
const params = new URLSearchParams()
|
||||
params.set('path', filePath)
|
||||
params.set('size', String(size))
|
||||
params.set('isdir', '0')
|
||||
params.set('autoinit', '1')
|
||||
params.set('rtype', String(rtype))
|
||||
params.set('block_list', JSON.stringify(blockList))
|
||||
if (contentMd5) params.set('content-md5', contentMd5)
|
||||
if (sliceMd5) params.set('slice-md5', sliceMd5)
|
||||
const resp = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
},
|
||||
body: params.toString()
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as BaiduPrecreateResp
|
||||
return data
|
||||
}
|
||||
|
||||
export const apiBaiduLocateUpload = async (
|
||||
user_id: string,
|
||||
filePath: string,
|
||||
uploadid: string
|
||||
): Promise<BaiduLocateUploadResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return null
|
||||
const url = new URL('/rest/2.0/pcs/file', BAIDU_PCS_HOST)
|
||||
url.searchParams.set('method', 'locateupload')
|
||||
url.searchParams.set('access_token', token.access_token)
|
||||
url.searchParams.set('appid', BAIDU_PCS_APP_ID)
|
||||
url.searchParams.set('path', filePath)
|
||||
url.searchParams.set('uploadid', uploadid)
|
||||
url.searchParams.set('upload_version', '2.0')
|
||||
const resp = await fetch(url.toString(), {
|
||||
headers: {
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
}
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as BaiduLocateUploadResp
|
||||
return data
|
||||
}
|
||||
|
||||
const buildMultipart = (fieldName: string, filename: string, buff: Buffer) => {
|
||||
const boundary = '----xbybaidu' + Date.now().toString(16) + Math.random().toString(16).slice(2)
|
||||
const parts: Buffer[] = []
|
||||
parts.push(Buffer.from(`--${boundary}\r\n`))
|
||||
parts.push(Buffer.from(`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"\r\n`))
|
||||
parts.push(Buffer.from(`Content-Type: application/octet-stream\r\n\r\n`))
|
||||
parts.push(buff)
|
||||
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`))
|
||||
const body = Buffer.concat(parts)
|
||||
return { body, boundary }
|
||||
}
|
||||
|
||||
export const apiBaiduUploadPart = async (
|
||||
server: string,
|
||||
accessToken: string,
|
||||
filePath: string,
|
||||
uploadid: string,
|
||||
partseq: number,
|
||||
buff: Buffer
|
||||
): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const host = server.startsWith('http') ? server : `https://${server}`
|
||||
const url = new URL('/rest/2.0/pcs/superfile2', host)
|
||||
url.searchParams.set('method', 'upload')
|
||||
url.searchParams.set('access_token', accessToken)
|
||||
url.searchParams.set('type', 'tmpfile')
|
||||
url.searchParams.set('path', filePath)
|
||||
url.searchParams.set('uploadid', uploadid)
|
||||
url.searchParams.set('partseq', String(partseq))
|
||||
const { body, boundary } = buildMultipart('file', path.basename(filePath), buff)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
protocol: url.protocol,
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length
|
||||
}
|
||||
}
|
||||
const req: ClientRequest = nodehttps.request(options, (res) => {
|
||||
let raw = ''
|
||||
res.on('data', (chunk) => {
|
||||
raw += chunk
|
||||
})
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
resolve(data?.errno === 0)
|
||||
} catch {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
req.on('error', () => resolve(false))
|
||||
req.write(body)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
export const apiBaiduCreateFile = async (
|
||||
user_id: string,
|
||||
filePath: string,
|
||||
size: number,
|
||||
uploadid: string,
|
||||
blockList: string[],
|
||||
rtype: number = 2
|
||||
): Promise<BaiduCreateResp | null> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return null
|
||||
const url = new URL('/rest/2.0/xpan/file', BAIDU_API_HOST)
|
||||
url.searchParams.set('method', 'create')
|
||||
url.searchParams.set('access_token', token.access_token)
|
||||
const params = new URLSearchParams()
|
||||
params.set('path', filePath)
|
||||
params.set('size', String(size))
|
||||
params.set('isdir', '0')
|
||||
params.set('uploadid', uploadid)
|
||||
params.set('rtype', String(rtype))
|
||||
params.set('block_list', JSON.stringify(blockList))
|
||||
const resp = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
},
|
||||
body: params.toString()
|
||||
})
|
||||
if (!resp.ok) return null
|
||||
const data = (await resp.json()) as BaiduCreateResp
|
||||
return data
|
||||
}
|
||||
|
||||
export const apiBaiduCreateDir = async (
|
||||
user_id: string,
|
||||
dirPath: string,
|
||||
rtype: number = 0
|
||||
): Promise<{ error: string; path: string }> => {
|
||||
const token = UserDAL.GetUserToken(user_id)
|
||||
if (!token?.access_token) return { error: '未登录百度网盘', path: '' }
|
||||
const url = new URL('/rest/2.0/xpan/file', BAIDU_API_HOST)
|
||||
url.searchParams.set('method', 'create')
|
||||
url.searchParams.set('access_token', token.access_token)
|
||||
const params = new URLSearchParams()
|
||||
params.set('path', dirPath)
|
||||
params.set('isdir', '1')
|
||||
params.set('rtype', String(rtype))
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
params.set('local_ctime', String(now))
|
||||
params.set('local_mtime', String(now))
|
||||
const resp = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'pan.baidu.com'
|
||||
},
|
||||
body: params.toString()
|
||||
})
|
||||
if (!resp.ok) return { error: '创建文件夹失败', path: '' }
|
||||
const data = (await resp.json()) as BaiduCreateResp
|
||||
if (data?.errno !== 0) {
|
||||
if (data.errno === -8) return { error: '文件夹已存在', path: '' }
|
||||
if (data.errno === -10) return { error: '云端容量已满', path: '' }
|
||||
if (data.errno === -7) return { error: '文件夹名错误或无权限', path: '' }
|
||||
return { error: '创建文件夹失败', path: '' }
|
||||
}
|
||||
return { error: '', path: data.path || dirPath }
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import path from 'path'
|
||||
import { OpenFileHandle } from '../utils/filehelper'
|
||||
import { IUploadingUI } from '../utils/dbupload'
|
||||
import UserDAL from '../user/userdal'
|
||||
import { Sleep } from '../utils/format'
|
||||
import AliUploadDisk from '../aliapi/uploaddisk'
|
||||
import {
|
||||
apiBaiduCreateFile,
|
||||
apiBaiduLocateUpload,
|
||||
apiBaiduPrecreate,
|
||||
apiBaiduUploadPart,
|
||||
buildBaiduUploadPath,
|
||||
computeBaiduBlockList
|
||||
} from './upload'
|
||||
|
||||
const mapCheckNameModeToRtype = (mode: string): number => {
|
||||
switch (mode) {
|
||||
case 'overwrite':
|
||||
return 1
|
||||
case 'auto_rename':
|
||||
return 2
|
||||
case 'refuse':
|
||||
return 0
|
||||
case 'ignore':
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
const readSlice = async (filePath: string, start: number, size: number) => {
|
||||
const handle = await OpenFileHandle(filePath)
|
||||
if (handle.error || !handle.handle) return { error: handle.error || '打开文件失败', buff: Buffer.alloc(0) }
|
||||
const buff = Buffer.alloc(size)
|
||||
const read = await handle.handle.read(buff, 0, size, start)
|
||||
await handle.handle.close()
|
||||
return { error: '', buff: buff.subarray(0, read.bytesRead) }
|
||||
}
|
||||
|
||||
export default class BaiduUploadDisk {
|
||||
static async UploadOneFile(fileui: IUploadingUI): Promise<string> {
|
||||
const token = await UserDAL.GetUserTokenFromDB(fileui.user_id)
|
||||
if (!token?.access_token) return '找不到上传token,请重试'
|
||||
|
||||
const filePath = path.join(fileui.localFilePath, fileui.File.partPath)
|
||||
const remotePath = buildBaiduUploadPath(fileui.parent_file_id || '/', fileui.File.name)
|
||||
|
||||
fileui.Info.uploadState = 'hashing'
|
||||
const fileHandle = await OpenFileHandle(filePath)
|
||||
if (fileHandle.error || !fileHandle.handle) return fileHandle.error || '打开文件失败'
|
||||
const { blockList, contentMd5, sliceMd5 } = await computeBaiduBlockList(fileHandle.handle, fileui.File.size)
|
||||
await fileHandle.handle.close()
|
||||
fileui.Info.uploadState = 'running'
|
||||
|
||||
if (!blockList.length) return '计算文件hash失败'
|
||||
|
||||
const rtype = mapCheckNameModeToRtype(fileui.check_name_mode)
|
||||
const precreate = await apiBaiduPrecreate(fileui.user_id, remotePath, fileui.File.size, blockList, contentMd5, sliceMd5, rtype)
|
||||
if (!precreate || precreate.errno !== 0) return '预上传失败'
|
||||
|
||||
const uploadid = precreate.uploadid || ''
|
||||
if (!uploadid) {
|
||||
fileui.File.uploaded_file_id = remotePath
|
||||
fileui.File.uploaded_is_rapid = true
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (precreate.return_type !== 2) {
|
||||
const locate = await apiBaiduLocateUpload(fileui.user_id, remotePath, uploadid)
|
||||
if (!locate || locate.errno !== 0 || !locate.host || locate.host.length === 0) return '获取上传服务器失败'
|
||||
const server = locate.host[0]
|
||||
const blockSize = 4 * 1024 * 1024
|
||||
const total = fileui.File.size
|
||||
let offset = 0
|
||||
let partseq = 0
|
||||
while (offset < total) {
|
||||
if (!fileui.IsRunning) return '已暂停'
|
||||
const size = Math.min(blockSize, total - offset)
|
||||
const slice = await readSlice(filePath, offset, size)
|
||||
if (slice.error) return slice.error
|
||||
let ok = false
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ok = await apiBaiduUploadPart(server, token.access_token, remotePath, uploadid, partseq, slice.buff)
|
||||
if (ok) break
|
||||
await Sleep(800)
|
||||
}
|
||||
if (!ok) return '分片上传失败'
|
||||
offset += size
|
||||
AliUploadDisk.RecordUploadProgress(fileui.UploadID, size, offset)
|
||||
partseq += 1
|
||||
}
|
||||
}
|
||||
|
||||
const create = await apiBaiduCreateFile(fileui.user_id, remotePath, fileui.File.size, uploadid, blockList, rtype)
|
||||
if (!create || create.errno !== 0) return '创建文件失败'
|
||||
fileui.File.uploaded_file_id = remotePath
|
||||
fileui.File.uploaded_is_rapid = precreate.return_type === 2
|
||||
return 'success'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div
|
||||
class="category-card"
|
||||
:class="`category-card--${type}`"
|
||||
@click="$emit('click', { name, type, count })"
|
||||
>
|
||||
<div class="category-card__background" :style="{ background: gradient }"></div>
|
||||
|
||||
<!-- 封面图层 -->
|
||||
<div v-if="coverImage" class="category-card__cover">
|
||||
<img :src="coverImage" :alt="name" />
|
||||
<div class="category-card__overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="category-card__content">
|
||||
<h3 class="category-card__name">{{ name }}</h3>
|
||||
<div class="category-card__count">
|
||||
<span class="category-card__number">{{ count }}</span>
|
||||
<span class="category-card__label">个项目</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string
|
||||
count: number
|
||||
type: 'genre' | 'rating' | 'year'
|
||||
gradient?: string
|
||||
coverImage?: string // 新增封面图属性
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'click', data: { name: string; type: string; count: number }): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||||
})
|
||||
|
||||
defineEmits<Emits>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.category-card {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-card__background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.category-card:hover .category-card__background {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 封面图样式 */
|
||||
.category-card__cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.category-card__cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.category-card:hover .category-card__cover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.category-card__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(0, 0, 0, 0.3) 0%,
|
||||
rgba(0, 0, 0, 0.6) 100%
|
||||
);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.category-card:hover .category-card__overlay {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.category-card__content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.category-card__name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-card__count {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.category-card__number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.category-card__label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 类型特定样式 */
|
||||
.category-card--genre {
|
||||
--gradient-from: #667eea;
|
||||
--gradient-to: #764ba2;
|
||||
}
|
||||
|
||||
.category-card--genre .category-card__background {
|
||||
background: linear-gradient(135deg, var(--gradient-from) 0%, var(--gradient-to) 100%);
|
||||
}
|
||||
|
||||
.category-card--rating {
|
||||
--gradient-from: #f093fb;
|
||||
--gradient-to: #f5576c;
|
||||
}
|
||||
|
||||
.category-card--rating .category-card__background {
|
||||
background: linear-gradient(135deg, var(--gradient-from) 0%, var(--gradient-to) 100%);
|
||||
}
|
||||
|
||||
.category-card--year {
|
||||
--gradient-from: #4facfe;
|
||||
--gradient-to: #00f2fe;
|
||||
}
|
||||
|
||||
.category-card--year .category-card__background {
|
||||
background: linear-gradient(135deg, var(--gradient-from) 0%, var(--gradient-to) 100%);
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.category-card {
|
||||
width: 160px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.category-card__content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.category-card__name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.category-card__number {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.category-card__label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
[arco-theme='dark'] .category-card {
|
||||
border-color: var(--color-neutral-4);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[arco-theme='dark'] .category-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="media-library-integration">
|
||||
<!-- 媒体库主界面 -->
|
||||
<div v-if="showMediaLibrary" class="media-library-main">
|
||||
<div class="library-header">
|
||||
<h2>媒体库</h2>
|
||||
<div class="header-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="showAddFolderModal = true"
|
||||
:loading="mediaStore.isScanning"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="iconfont iconadd" />
|
||||
</template>
|
||||
添加文件夹
|
||||
</a-button>
|
||||
|
||||
<a-button
|
||||
type="text"
|
||||
@click="refreshLibrary"
|
||||
:loading="mediaStore.isScanning"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="iconfont iconreload-1-icon" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="library-content">
|
||||
<!-- 继续观看 -->
|
||||
<div class="content-section" v-if="mediaStore.continueWatching.length > 0">
|
||||
<h3>继续观看</h3>
|
||||
<div class="media-row">
|
||||
<div
|
||||
v-for="item in mediaStore.continueWatching.slice(0, 6)"
|
||||
:key="item.id"
|
||||
class="media-card"
|
||||
@click="playMedia(item)"
|
||||
>
|
||||
<div class="poster">
|
||||
<img v-if="item.posterUrl" :src="item.posterUrl" :alt="item.name" />
|
||||
<div v-else class="poster-placeholder">
|
||||
<i class="iconfont iconfile-video" />
|
||||
</div>
|
||||
<div class="progress-bar" v-if="item.watchProgress">
|
||||
<div class="progress-fill" :style="{ width: item.watchProgress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4>{{ item.name }}</h4>
|
||||
<p v-if="item.year">{{ item.year }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近添加 -->
|
||||
<div class="content-section" v-if="mediaStore.recentlyAdded.length > 0">
|
||||
<h3>最近添加</h3>
|
||||
<div class="media-row">
|
||||
<div
|
||||
v-for="item in mediaStore.recentlyAdded.slice(0, 12)"
|
||||
:key="item.id"
|
||||
class="media-card"
|
||||
@click="playMedia(item)"
|
||||
>
|
||||
<div class="poster">
|
||||
<img v-if="item.posterUrl" :src="item.posterUrl" :alt="item.name" />
|
||||
<div v-else class="poster-placeholder">
|
||||
<i class="iconfont iconfile-video" />
|
||||
</div>
|
||||
<div class="type-badge">{{ item.type === 'movie' ? '电影' : '电视剧' }}</div>
|
||||
<div class="rating" v-if="item.rating">{{ item.rating.toFixed(1) }}</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4>{{ item.name }}</h4>
|
||||
<p>{{ item.year }} · {{ item.genres.slice(0, 2).join(', ') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类展示 -->
|
||||
<div class="content-section">
|
||||
<a-tabs v-model:activeKey="activeTab" type="card">
|
||||
<a-tab-pane key="movies" tab="电影">
|
||||
<MediaGrid :items="mediaStore.movies" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tv" tab="电视剧">
|
||||
<MediaGrid :items="mediaStore.tvShows" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="unmatched" tab="未匹配">
|
||||
<MediaGrid :items="mediaStore.unmatchedItems" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加文件夹对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="showAddFolderModal"
|
||||
title="添加到媒体库"
|
||||
@ok="handleAddFolder"
|
||||
@cancel="showAddFolderModal = false"
|
||||
>
|
||||
<a-form :model="addFolderForm" layout="vertical">
|
||||
<a-form-item label="选择文件夹">
|
||||
<a-tree-select
|
||||
v-model:value="addFolderForm.folderId"
|
||||
:tree-data="folderTreeData"
|
||||
placeholder="请选择要添加的文件夹"
|
||||
tree-default-expand-all
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="媒体库名称">
|
||||
<a-input v-model:value="addFolderForm.name" placeholder="请输入媒体库显示名称" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useMediaLibraryStore } from '../store/medialibrary'
|
||||
import { MediaScanner } from '../utils/mediaScanner'
|
||||
import type { MediaLibraryItem } from '../types/media'
|
||||
import MediaGrid from './MediaGrid.vue'
|
||||
|
||||
const mediaStore = useMediaLibraryStore()
|
||||
const mediaScanner = MediaScanner.getInstance()
|
||||
|
||||
// 状态
|
||||
const showMediaLibrary = ref(true)
|
||||
const showAddFolderModal = ref(false)
|
||||
const activeTab = ref('movies')
|
||||
const addFolderForm = ref({
|
||||
folderId: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const folderTreeData = computed(() => {
|
||||
// 这里应该从实际的文件夹结构生成
|
||||
return [
|
||||
{
|
||||
title: '备份盘',
|
||||
value: 'backup',
|
||||
children: [
|
||||
{ title: '电影', value: 'backup_movies' },
|
||||
{ title: '电视剧', value: 'backup_tv' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '资源盘',
|
||||
value: 'resource',
|
||||
children: [
|
||||
{ title: '电影', value: 'resource_movies' },
|
||||
{ title: '电视剧', value: 'resource_tv' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 方法
|
||||
const playMedia = (item: MediaLibraryItem) => {
|
||||
console.log('Playing media:', item.name)
|
||||
// 这里可以集成播放器
|
||||
}
|
||||
|
||||
const refreshLibrary = () => {
|
||||
console.log('Refreshing media library...')
|
||||
}
|
||||
|
||||
const handleAddFolder = async () => {
|
||||
if (!addFolderForm.value.folderId || !addFolderForm.value.name) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里需要调用实际的文件夹扫描
|
||||
console.log('Adding folder:', addFolderForm.value)
|
||||
showAddFolderModal.value = false
|
||||
addFolderForm.value = { folderId: '', name: '' }
|
||||
} catch (error) {
|
||||
console.error('Error adding folder:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
console.log('Media library integration mounted')
|
||||
})
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
showAddFolderModal
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.media-library-integration {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.library-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
|
||||
.library-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.library-content {
|
||||
padding: 24px;
|
||||
height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.media-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.media-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.media-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.poster {
|
||||
position: relative;
|
||||
aspect-ratio: 2/3;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
|
||||
.poster img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.poster-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--color-text-3);
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: rgb(var(--primary-6));
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
background: rgba(var(--primary-6), 0.8);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rating {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.library-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.media-row {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,314 @@
|
||||
<!-- MediaPanRight.vue - 媒体库专用的文件列表组件 -->
|
||||
<template>
|
||||
<div class='media-pan-right'>
|
||||
<!-- 加载状态 -->
|
||||
<a-skeleton v-if='mediaPanFileStore.ListLoading && mediaPanFileStore.ListDataCount == 0' :loading='true' :animation='true'>
|
||||
<a-skeleton-line :rows='10' :line-height='50' :line-spacing='50' />
|
||||
</a-skeleton>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<a-list
|
||||
v-else
|
||||
ref='viewlist'
|
||||
:bordered='false'
|
||||
:split='false'
|
||||
:max-height='500'
|
||||
:virtual-list-props="{
|
||||
height: 500,
|
||||
fixedSize: true,
|
||||
estimatedSize: 50,
|
||||
threshold: 1,
|
||||
itemKey: 'file_id'
|
||||
}"
|
||||
style='width: 100%'
|
||||
:data='mediaPanFileStore.ListDataShow'
|
||||
tabindex='-1'
|
||||
@scroll='handleListScroll'>
|
||||
<template #empty>
|
||||
<a-empty description='空文件夹' />
|
||||
</template>
|
||||
<template #item='{ item, index }'>
|
||||
<div :key="'l-' + item.file_id" class='listitemdiv'>
|
||||
<div
|
||||
v-if='item.isDir'
|
||||
:class="'fileitem' + (mediaPanFileStore.ListSelected.has(item.file_id) ? ' selected' : '') + (mediaPanFileStore.ListFocusKey == item.file_id ? ' focus' : '')"
|
||||
@click='handleSelect(item.file_id, $event)'
|
||||
@dblclick='handleOpenFile($event, item)'>
|
||||
<!-- 选择框 -->
|
||||
<div class='rangselect'>
|
||||
<a-button shape='circle' type='text' tabindex='-1' class='select' :title='index'
|
||||
@click.prevent.stop='handleSelect(item.file_id, $event, true)'>
|
||||
<i
|
||||
:class="mediaPanFileStore.ListSelected.has(item.file_id) ? (item.starred ? 'iconfont iconcrown3' : 'iconfont iconrsuccess') : item.starred ? 'iconfont iconcrown' : 'iconfont iconpic2'" />
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 文件图标 -->
|
||||
<div class='fileicon'>
|
||||
<i :class="'iconfont ' + item.icon" aria-hidden='true'></i>
|
||||
</div>
|
||||
<!-- 文件名 -->
|
||||
<div class='filename' droppable='false'>
|
||||
<div @click='handleOpenFile($event, item)'>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件按钮 -->
|
||||
<div class='filebtn'>
|
||||
<a-popover v-if='item.thumbnail'
|
||||
content-class='popimg' position='lt'>
|
||||
<a-button type='text' tabindex='-1' class='gengduo' title='缩略图'>
|
||||
<i class='iconfont icongengduo' />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<div class='preimg'>
|
||||
<img :src='item.thumbnail' onerror="javascript:this.src='imgerror.png';" />
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<a-button v-else type='text' tabindex='-1' class='gengduo' disabled></a-button>
|
||||
</div>
|
||||
<!-- 文件大小 -->
|
||||
<div class='filesize'>{{ item.sizeStr }}</div>
|
||||
<div v-show='item.file_count' class='filesize'>{{ '文件数: ' + item.file_count }}</div>
|
||||
<!-- 修改时间 -->
|
||||
<div class='filetime'>{{ item.timeStr }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件项 -->
|
||||
<div
|
||||
v-else
|
||||
:class="'fileitem' + (mediaPanFileStore.ListSelected.has(item.file_id) ? ' selected' : '') + (mediaPanFileStore.ListFocusKey == item.file_id ? ' focus' : '')"
|
||||
@click='handleSelect(item.file_id, $event)'
|
||||
@dblclick='handleOpenFile($event, item)'>
|
||||
<!-- 选择框 -->
|
||||
<div class='rangselect'>
|
||||
<a-button shape='circle' type='text' tabindex='-1' class='select' :title='index'
|
||||
@click.prevent.stop='handleSelect(item.file_id, $event, true)'>
|
||||
<i
|
||||
:class="mediaPanFileStore.ListSelected.has(item.file_id) ? (item.starred ? 'iconfont iconcrown3' : 'iconfont iconrsuccess') : item.starred ? 'iconfont iconcrown' : 'iconfont iconpic2'" />
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 文件图标 -->
|
||||
<div class='fileicon'>
|
||||
<i :class="'iconfont ' + item.icon" aria-hidden='true'></i>
|
||||
</div>
|
||||
<!-- 文件名 -->
|
||||
<div class='filename' droppable='false'>
|
||||
<div @click='handleOpenFile($event, item)'>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文件按钮 -->
|
||||
<div class='filebtn'>
|
||||
<a-popover v-if='item.thumbnail' content-class='popimg'
|
||||
position='lt'>
|
||||
<a-button type='text' tabindex='-1' class='gengduo'>
|
||||
<i class='iconfont icontupianyulan' />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<div class='preimg'>
|
||||
<img :src='item.thumbnail' onerror="javascript:this.src='imgerror.png';" />
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<a-button v-else type='text' tabindex='-1' class='gengduo' disabled></a-button>
|
||||
</div>
|
||||
<!-- 文件大小 -->
|
||||
<div class='filesize'>
|
||||
{{ item.sizeStr }}
|
||||
</div>
|
||||
<!-- 修改时间 -->
|
||||
<div class='filetime'>{{ item.timeStr }}</div>
|
||||
<!-- 媒体信息 -->
|
||||
<div class='filesize' v-show="item.media_duration || item.media_play_cursor">
|
||||
<span>{{ '总时:' + (item.media_duration || '未知时长') }}</span>
|
||||
<span>{{ '观看:' + (item.media_play_cursor || '未知状态') }}</span>
|
||||
<span>{{ item.media_width > 0 ? item.media_width + 'x' + item.media_height : '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue'
|
||||
import { IAliGetFileModel } from '../aliapi/alimodels'
|
||||
import { useMediaPanFileStore } from './stores'
|
||||
import { menuOpenFile } from '../utils/openfile'
|
||||
|
||||
// 定义 emit 事件
|
||||
const emit = defineEmits<{
|
||||
enterFolder: [file: IAliGetFileModel]
|
||||
}>()
|
||||
|
||||
const viewlist = ref()
|
||||
const mediaPanFileStore = useMediaPanFileStore()
|
||||
|
||||
const handleListScroll = () => {
|
||||
// 处理滚动事件(媒体库中不需要复杂的滚动逻辑)
|
||||
}
|
||||
|
||||
const handleSelect = (file_id: string, event: MouseEvent, isCtrl: boolean = false) => {
|
||||
mediaPanFileStore.mMouseSelect(file_id, event.ctrlKey || isCtrl, event.shiftKey)
|
||||
if (!mediaPanFileStore.ListSelected.has(file_id)) mediaPanFileStore.ListFocusKey = ''
|
||||
}
|
||||
|
||||
const handleOpenFile = (event: Event, file: IAliGetFileModel | undefined) => {
|
||||
if (!file) file = mediaPanFileStore.GetSelectedFirst()
|
||||
if (!file) return
|
||||
|
||||
// 如果是文件夹,应该进入子目录而不是打开文件
|
||||
if (file.isDir) {
|
||||
// 发出事件让 MediaLibrary.vue 处理文件夹导航
|
||||
emit('enterFolder', file)
|
||||
return
|
||||
}
|
||||
|
||||
// 只有文件才执行打开操作
|
||||
if (!mediaPanFileStore.ListSelected.has(file.file_id)) {
|
||||
mediaPanFileStore.mMouseSelect(file.file_id, false, false)
|
||||
}
|
||||
|
||||
// 简化的文件打开逻辑
|
||||
menuOpenFile(file)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
viewlist
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.media-pan-right {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.listitemdiv {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fileitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
min-height: 50px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.fileitem:hover {
|
||||
background-color: var(--color-neutral-1);
|
||||
}
|
||||
|
||||
.fileitem.selected {
|
||||
background-color: var(--color-primary-light-1);
|
||||
}
|
||||
|
||||
.fileitem.focus {
|
||||
background-color: var(--color-primary-light-2);
|
||||
}
|
||||
|
||||
.rangselect {
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rangselect .select {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.fileicon {
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.fileicon .iconfont {
|
||||
font-size: 20px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.filename {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.filename > div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.filebtn {
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.filebtn .gengduo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.filesize {
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
text-align: right;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.filetime {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.preimg {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.preimg img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.filesize,
|
||||
.filetime {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filename {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,348 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { IAliGetFileModel } from '../../aliapi/alimodels'
|
||||
import { ArrayToMap } from '../../utils/utils'
|
||||
import fuzzysort from 'fuzzysort'
|
||||
import {
|
||||
GetFocusNext,
|
||||
GetSelectedList,
|
||||
GetSelectedListID,
|
||||
KeyboardSelectOne,
|
||||
MouseSelectOne,
|
||||
SelectAll
|
||||
} from '../../utils/selecthelper'
|
||||
import { OrderDir } from '../../utils/filenameorder'
|
||||
import { onHideRightMenuScroll } from '../../utils/keyboardhelper'
|
||||
|
||||
type Item = IAliGetFileModel
|
||||
|
||||
export interface GridItem {
|
||||
file_id: string
|
||||
files: IAliGetFileModel[]
|
||||
}
|
||||
|
||||
export interface MediaPanFileState {
|
||||
DriveID: string
|
||||
DirID: string
|
||||
AlbumID: string
|
||||
DirName: string
|
||||
|
||||
ListLoading: boolean
|
||||
ListLoadingIndex: number
|
||||
|
||||
ListDataRaw: Item[]
|
||||
|
||||
ListDataShow: Item[]
|
||||
ListDataGrid: GridItem[]
|
||||
|
||||
ListSelected: Set<string>
|
||||
|
||||
ListOrderKey: string
|
||||
|
||||
ListFocusKey: string
|
||||
|
||||
ListSelectKey: string
|
||||
|
||||
ListSearchKey: string
|
||||
|
||||
ListShowMode: string
|
||||
ListShowColumn: number
|
||||
|
||||
scrollToFile: string
|
||||
}
|
||||
|
||||
type State = MediaPanFileState
|
||||
const KEY = 'file_id'
|
||||
|
||||
const useMediaPanFileStore = defineStore('mediaPanFile', {
|
||||
state: (): State => ({
|
||||
DriveID: '',
|
||||
DirID: '',
|
||||
AlbumID: '',
|
||||
DirName: '',
|
||||
|
||||
ListLoading: false,
|
||||
ListLoadingIndex: 0,
|
||||
ListDataRaw: [],
|
||||
ListDataShow: [],
|
||||
ListDataGrid: [],
|
||||
ListSelected: new Set<string>(),
|
||||
ListOrderKey: 'name asc',
|
||||
ListFocusKey: '',
|
||||
ListSelectKey: '',
|
||||
ListSearchKey: '',
|
||||
ListShowMode: 'list',
|
||||
ListShowColumn: 1,
|
||||
scrollToFile: ''
|
||||
}),
|
||||
|
||||
getters: {
|
||||
ListDataCount(state: State): number {
|
||||
return state.ListDataShow.length
|
||||
},
|
||||
|
||||
IsListSelected(state: State): boolean {
|
||||
return state.ListSelected.size > 0
|
||||
},
|
||||
|
||||
IsListSelectedMulti(state: State): boolean {
|
||||
let isMulti = state.ListSelected.size > 1
|
||||
if (isMulti && state.DirID === 'mypic') {
|
||||
return false
|
||||
}
|
||||
return isMulti
|
||||
},
|
||||
ListSelectedCount(state: State): number {
|
||||
return state.ListSelected.size
|
||||
},
|
||||
ListDataSelectCountInfo(state: State): string {
|
||||
return '已选中 ' + state.ListSelected.size + ' / ' + state.ListDataShow.length + ' 个'
|
||||
},
|
||||
|
||||
IsListSelectedAll(state: State): boolean {
|
||||
return state.ListSelected.size > 0 && state.ListSelected.size == state.ListDataShow.length
|
||||
},
|
||||
|
||||
IsListSelectedFavAll(state: State): boolean {
|
||||
const list = state.ListDataShow
|
||||
const len = list.length
|
||||
let isAllFav = true
|
||||
|
||||
for (let i = 0, maxi = len; i < maxi; i++) {
|
||||
if (state.ListSelected.has(list[i].file_id)) {
|
||||
if (!list[i].starred) {
|
||||
isAllFav = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAllFav
|
||||
},
|
||||
|
||||
SelectDirType(state: State): string {
|
||||
const file_id = state.DirID
|
||||
if (file_id == 'recover') return 'recover'
|
||||
if (file_id == 'trash') return 'trash'
|
||||
if (file_id == 'favorite') return 'favorite'
|
||||
if (state.AlbumID) return 'mypic'
|
||||
if (file_id == 'pic' || file_id == 'pic_root' || file_id == 'mypic') return 'pic'
|
||||
if (file_id.startsWith('search')) return 'search'
|
||||
if (file_id.startsWith('color')) return 'color'
|
||||
if (file_id.startsWith('video')) return 'video'
|
||||
return 'pan'
|
||||
},
|
||||
FileOrderDesc(state: State): string {
|
||||
switch (state.ListOrderKey) {
|
||||
case 'name desc':
|
||||
return '名称 · 降'
|
||||
case 'name asc':
|
||||
return '名称 · 升'
|
||||
case 'updated_at desc':
|
||||
return '时间 · 降'
|
||||
case 'updated_at asc':
|
||||
return '时间 · 升'
|
||||
case 'size desc':
|
||||
return '大小 · 降'
|
||||
case 'size asc':
|
||||
return '大小 · 升'
|
||||
case 'file_count desc':
|
||||
return '数量 · 降'
|
||||
case 'file_count asc':
|
||||
return '数量 · 升'
|
||||
}
|
||||
return '选择文件排序'
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
mSaveDirFileLoading(drive_id: string, dirID: string, dirName: string, albumID: string = '') {
|
||||
if (this.DirID != dirID || this.DriveID != drive_id || this.AlbumID != albumID) {
|
||||
this.$patch({
|
||||
DriveID: drive_id,
|
||||
DirID: dirID,
|
||||
AlbumID: albumID,
|
||||
DirName: dirName,
|
||||
ListOrderKey: 'name asc',
|
||||
ListLoading: true,
|
||||
ListLoadingIndex: 0,
|
||||
ListSearchKey: '',
|
||||
ListDataRaw: [],
|
||||
ListDataShow: [],
|
||||
ListDataGrid: [],
|
||||
ListSelected: new Set(),
|
||||
ListFocusKey: '',
|
||||
ListSelectKey: ''
|
||||
})
|
||||
} else {
|
||||
this.$patch({
|
||||
DriveID: drive_id,
|
||||
DirID: dirID,
|
||||
AlbumID: albumID,
|
||||
DirName: dirName,
|
||||
ListOrderKey: 'name asc',
|
||||
ListLoading: true,
|
||||
ListLoadingIndex: 0,
|
||||
ListSearchKey: '',
|
||||
ListDataRaw: [],
|
||||
ListDataShow: [],
|
||||
ListDataGrid: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mSaveDirFileLoadingFinish(drive_id: string, dirID: string, list: Item[], itemsTotal: number = 0) {
|
||||
if (this.DirID && (drive_id != this.DriveID || dirID != this.DirID)) return
|
||||
|
||||
this.ListDataRaw = list
|
||||
this.$patch({ ListLoading: false, ListLoadingIndex: 0 })
|
||||
this.mRefreshListDataShow(true)
|
||||
},
|
||||
|
||||
mSearchListData(value: string) {
|
||||
this.$patch({ ListSearchKey: value })
|
||||
this.mRefreshListDataShow(true)
|
||||
},
|
||||
|
||||
mOrderListData(value: string) {
|
||||
if (!value || value == this.ListOrderKey) return
|
||||
this.$patch({ ListOrderKey: value, ListSelected: new Set<string>(), ListFocusKey: '', ListSelectKey: '' })
|
||||
this.mRefreshListDataShow(true)
|
||||
},
|
||||
|
||||
mGridListData(value: string, column: number) {
|
||||
if (this.ListShowMode == value && this.ListShowColumn == column) return
|
||||
this.$patch({ ListShowMode: value == 'list' ? 'list' : 'grid', ListShowColumn: value == 'list' ? 1 : column })
|
||||
this.mRefreshListDataShow(true)
|
||||
},
|
||||
|
||||
mRefreshListDataShow(refreshRaw: boolean) {
|
||||
if (!refreshRaw) {
|
||||
const listDataShow = this.ListDataShow.concat()
|
||||
Object.freeze(listDataShow)
|
||||
const listDataGrid = this.ListDataGrid.concat()
|
||||
Object.freeze(listDataGrid)
|
||||
this.$patch({ ListDataShow: listDataShow, ListDataGrid: listDataGrid })
|
||||
return
|
||||
}
|
||||
let showList: Item[] = []
|
||||
if (this.ListSearchKey) {
|
||||
const results = fuzzysort.go(this.ListSearchKey, this.ListDataRaw, {
|
||||
threshold: -200000,
|
||||
keys: ['name', 'namesearch'],
|
||||
scoreFn: (a) => Math.max(a[0] ? a[0].score : -200000, a[1] ? a[1].score : -200000)
|
||||
})
|
||||
for (let i = 0, maxi = results.length; i < maxi; i++) {
|
||||
if (results[i].score > -200000) showList.push(results[i].obj)
|
||||
}
|
||||
// 重新排序
|
||||
const orders = this.ListOrderKey
|
||||
.replace(' desc', ' DESC')
|
||||
.replace(' asc', ' ASC')
|
||||
.split(' ')
|
||||
OrderDir(orders[0], orders[1], showList)
|
||||
} else {
|
||||
showList = this.ListDataRaw.concat()
|
||||
}
|
||||
Object.freeze(showList)
|
||||
const gridList: GridItem[] = []
|
||||
const column = this.ListShowColumn
|
||||
for (let i = 0, maxi = showList.length; i < maxi; i += column) {
|
||||
const grid: GridItem = {
|
||||
file_id: showList[i].file_id,
|
||||
files: [showList[i]]
|
||||
}
|
||||
for (let j = 1; j < column && i + j < maxi; j++) {
|
||||
grid.files.push(showList[i + j])
|
||||
}
|
||||
gridList.push(grid)
|
||||
}
|
||||
Object.freeze(gridList)
|
||||
const oldSelected = this.ListSelected
|
||||
const newSelected = new Set<string>()
|
||||
let key = ''
|
||||
for (let i = 0, maxi = showList.length; i < maxi; i++) {
|
||||
key = showList[i][KEY]
|
||||
if (oldSelected.has(key)) newSelected.add(key)
|
||||
}
|
||||
this.$patch({ ListDataShow: showList, ListDataGrid: gridList, ListSelected: newSelected })
|
||||
},
|
||||
|
||||
mSelectAll() {
|
||||
if (!this.ListDataShow.length) return
|
||||
let selectKey = this.ListDataShow[0].file_id
|
||||
let ListSelected = SelectAll(this.ListDataShow, KEY, this.ListSelected)
|
||||
if (this.ListDataShow.length === this.ListSelected.size) selectKey = ''
|
||||
this.$patch({ ListSelected: ListSelected, ListFocusKey: selectKey, ListSelectKey: selectKey })
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
mMouseSelect(key: string, Ctrl: boolean, Shift: boolean) {
|
||||
if (this.ListDataShow.length == 0) return
|
||||
const data = MouseSelectOne(this.ListDataShow, KEY, this.ListSelected, this.ListFocusKey, this.ListSelectKey, key, Ctrl, Shift, '')
|
||||
this.$patch({ ListSelected: data.selectedNew, ListFocusKey: data.focusLast, ListSelectKey: data.selectedLast })
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
mKeyboardSelect(key: string, Ctrl: boolean, Shift: boolean) {
|
||||
if (this.ListDataShow.length == 0) return
|
||||
const data = KeyboardSelectOne(this.ListDataShow, KEY, this.ListSelected, this.ListFocusKey, this.ListSelectKey, key, Ctrl, Shift, '')
|
||||
this.$patch({ ListSelected: data.selectedNew, ListFocusKey: data.focusLast, ListSelectKey: data.selectedLast })
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
mRangSelect(lastkey: string, file_idList: string[]) {
|
||||
if (this.ListDataShow.length == 0) return
|
||||
const selectedNew = new Set<string>(this.ListSelected)
|
||||
for (let i = 0, maxi = file_idList.length; i < maxi; i++) {
|
||||
selectedNew.add(file_idList[i])
|
||||
}
|
||||
this.$patch({ ListSelected: selectedNew, ListFocusKey: lastkey, ListSelectKey: lastkey })
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
mCancelSelect() {
|
||||
onHideRightMenuScroll()
|
||||
this.ListSelected.clear()
|
||||
this.ListFocusKey = ''
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
GetSelected() {
|
||||
return GetSelectedList(this.ListDataShow, KEY, this.ListSelected)
|
||||
},
|
||||
|
||||
GetSelectedID() {
|
||||
return GetSelectedListID(this.ListDataShow, KEY, this.ListSelected)
|
||||
},
|
||||
|
||||
GetSelectedParentDirID() {
|
||||
return GetSelectedListID(this.ListDataShow, 'parent_file_id', this.ListSelected)
|
||||
},
|
||||
|
||||
GetSelectedFirst(): Item | undefined {
|
||||
const list = GetSelectedList(this.ListDataShow, KEY, this.ListSelected)
|
||||
if (list.length > 0) return list[0]
|
||||
return undefined
|
||||
},
|
||||
|
||||
mSetFocus(key: string) {
|
||||
this.ListFocusKey = key
|
||||
this.mRefreshListDataShow(false)
|
||||
},
|
||||
|
||||
mGetFocus(): string {
|
||||
if (!this.ListFocusKey && this.ListDataShow.length > 0) return this.ListDataShow[0][KEY]
|
||||
return this.ListFocusKey
|
||||
},
|
||||
|
||||
mGetFocusNext(position: string): string {
|
||||
return GetFocusNext(this.ListDataShow, KEY, this.ListFocusKey, position, '')
|
||||
},
|
||||
|
||||
mSaveFileScrollTo(file_id: string) {
|
||||
if (file_id == 'refresh') file_id = this.ListSelectKey
|
||||
this.scrollToFile = file_id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default useMediaPanFileStore
|
||||
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { IAliGetDirModel } from '../../aliapi/alimodels'
|
||||
import { h } from 'vue'
|
||||
|
||||
export interface MediaPanTreeState {
|
||||
user_id: string
|
||||
drive_id: string
|
||||
|
||||
selectDir: IAliGetDirModel
|
||||
|
||||
selectDirPath: IAliGetDirModel[]
|
||||
}
|
||||
|
||||
type State = MediaPanTreeState
|
||||
|
||||
export const fileiconfn = (icon: string) => h('i', { class: 'iconfont ' + icon })
|
||||
|
||||
const useMediaPanTreeStore = defineStore('mediaPanTree', {
|
||||
state: (): State => ({
|
||||
user_id: '',
|
||||
drive_id: '',
|
||||
selectDir: {
|
||||
__v_skip: true,
|
||||
drive_id: '',
|
||||
file_id: '',
|
||||
album_id: '',
|
||||
album_type: '',
|
||||
parent_file_id: '',
|
||||
name: '',
|
||||
namesearch: '',
|
||||
path: '',
|
||||
size: 0,
|
||||
time: 0,
|
||||
description: ''
|
||||
},
|
||||
selectDirPath: []
|
||||
}),
|
||||
|
||||
getters: {},
|
||||
|
||||
actions: {
|
||||
mSaveUser(user_id: string, drive_id: string) {
|
||||
this.$patch({ user_id, drive_id })
|
||||
},
|
||||
|
||||
mShowDir(dir: IAliGetDirModel, dirPath: IAliGetDirModel[]) {
|
||||
this.$patch({
|
||||
selectDir: dir,
|
||||
selectDirPath: dirPath
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default useMediaPanTreeStore
|
||||
@@ -0,0 +1,10 @@
|
||||
import useMediaPanFileStore, { type MediaPanFileState, type GridItem } from './MediaPanFileStore'
|
||||
import useMediaPanTreeStore, { type MediaPanTreeState } from './MediaPanTreeStore'
|
||||
|
||||
export {
|
||||
useMediaPanFileStore,
|
||||
useMediaPanTreeStore,
|
||||
type MediaPanFileState,
|
||||
type MediaPanTreeState,
|
||||
type GridItem
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { modalCloseAll, modalSelectPanDir } from '../utils/modal'
|
||||
import { useModalStore, useUserStore } from '../store'
|
||||
import { isCloud123User } from '../aliapi/utils'
|
||||
import message from '../utils/message'
|
||||
import DownDAL from './DownDAL'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const okLoading = ref(false)
|
||||
const modalStore = useModalStore()
|
||||
const form = reactive({
|
||||
url: '',
|
||||
fileName: '',
|
||||
dirId: '',
|
||||
dirName: '默认(来自:离线下载)'
|
||||
})
|
||||
|
||||
const handleOpen = () => {
|
||||
const preset = modalStore.modalData?.offlineForm
|
||||
if (preset) {
|
||||
form.url = preset.url || ''
|
||||
form.fileName = preset.fileName || ''
|
||||
form.dirId = preset.dirId || ''
|
||||
form.dirName = preset.dirName || '默认(来自:离线下载)'
|
||||
} else {
|
||||
form.url = ''
|
||||
form.fileName = ''
|
||||
form.dirId = ''
|
||||
form.dirName = '默认(来自:离线下载)'
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (okLoading.value) okLoading.value = false
|
||||
}
|
||||
|
||||
const handleHide = () => {
|
||||
modalCloseAll()
|
||||
}
|
||||
|
||||
const handleSelectDir = () => {
|
||||
const snapshot = {
|
||||
url: form.url,
|
||||
fileName: form.fileName,
|
||||
dirId: form.dirId,
|
||||
dirName: form.dirName
|
||||
}
|
||||
modalSelectPanDir('offline', form.dirId, (user_id: string, drive_id: string, selectFile: any) => {
|
||||
if (!selectFile || selectFile.isDir !== true) return
|
||||
if (selectFile.file_id && String(selectFile.file_id).includes('root')) {
|
||||
snapshot.dirId = ''
|
||||
snapshot.dirName = '默认(来自:离线下载)'
|
||||
} else {
|
||||
snapshot.dirId = String(selectFile.file_id || '')
|
||||
snapshot.dirName = selectFile.name || '已选择'
|
||||
}
|
||||
modalStore.showModal('cloud123offline', { offlineForm: snapshot })
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
const user = useUserStore().user_id
|
||||
if (!isCloud123User(user)) {
|
||||
message.error('当前账号不是123云盘')
|
||||
return
|
||||
}
|
||||
const url = form.url.trim()
|
||||
if (!url) {
|
||||
message.error('请输入离线下载地址')
|
||||
return
|
||||
}
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
message.error('仅支持 http/https 链接')
|
||||
return
|
||||
}
|
||||
okLoading.value = true
|
||||
const result = await DownDAL.aAddCloud123OfflineDownload(url, form.fileName.trim(), form.dirId)
|
||||
okLoading.value = false
|
||||
if (!result.success) {
|
||||
message.error(result.message || '创建离线下载失败')
|
||||
return
|
||||
}
|
||||
message.success('离线下载任务已创建')
|
||||
modalCloseAll()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="props.visible"
|
||||
modal-class="modalclass"
|
||||
:footer="false"
|
||||
:unmount-on-close="true"
|
||||
:mask-closable="false"
|
||||
@cancel="handleHide"
|
||||
@before-open="handleOpen"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #title>
|
||||
<span class="modaltitle">创建离线下载任务</span>
|
||||
</template>
|
||||
<div style="width: 520px">
|
||||
<a-form ref="formRef" :model="form" layout="vertical">
|
||||
<a-form-item field="url" label="下载链接">
|
||||
<a-input v-model.trim="form.url" placeholder="http/https 链接" />
|
||||
</a-form-item>
|
||||
<a-form-item field="fileName" label="自定义文件名(可选)">
|
||||
<a-input v-model.trim="form.fileName" placeholder="例如:视频.mp4" />
|
||||
</a-form-item>
|
||||
<a-form-item field="dirId" label="保存到">
|
||||
<a-input-search
|
||||
:readonly="true"
|
||||
button-text="选择文件夹"
|
||||
search-button
|
||||
:model-value="form.dirName"
|
||||
@search="handleSelectDir"
|
||||
/>
|
||||
<div style="margin-top: 6px; color: var(--color-text-3); font-size: 12px">
|
||||
根目录不支持离线下载,未选择时将保存到“来自:离线下载”文件夹
|
||||
</div>
|
||||
</a-form-item>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<a-button type="outline" @click="handleHide">取消</a-button>
|
||||
<a-button type="primary" :loading="okLoading" @click="handleCreate">创建</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
@@ -19,6 +19,7 @@ import DBDown from '../utils/dbdown'
|
||||
import fsPromises from 'fs/promises'
|
||||
import { DecodeEncName } from '../aliapi/utils'
|
||||
import { getEncType } from '../utils/proxyhelper'
|
||||
import { SHA256 } from 'crypto-js'
|
||||
|
||||
export interface IStateDownFile {
|
||||
DownID: string
|
||||
@@ -66,6 +67,11 @@ export interface IStateDownInfo {
|
||||
sha1: string
|
||||
|
||||
crc64: string
|
||||
|
||||
localFilePath?: string
|
||||
offlineProvider?: 'cloud123'
|
||||
offlineTaskId?: string
|
||||
offlineDirId?: string
|
||||
}
|
||||
|
||||
export interface IAriaDownProgress {
|
||||
@@ -86,6 +92,11 @@ const sound = new Howl({
|
||||
volume: 1.0 // 音量,范围 0.0 ~ 1.0
|
||||
})
|
||||
|
||||
const buildAriaTaskGid = (file: IAliGetFileModel) => {
|
||||
const source = `${file.drive_id || ''}|${file.file_id || ''}|${file.size || 0}`
|
||||
return SHA256(source).toString().toLowerCase().replace(/[^0-9a-f]/g, '').slice(0, 16)
|
||||
}
|
||||
|
||||
export default class DownDAL {
|
||||
|
||||
/**
|
||||
@@ -181,15 +192,14 @@ export default class DownDAL {
|
||||
else fullPath = fullPath.replace(/\//g, '\\')
|
||||
}
|
||||
|
||||
let sizehex = file.size.toString(16).toLowerCase()
|
||||
if (sizehex.length > 4) sizehex = sizehex.substr(0, 4)
|
||||
const gid = buildAriaTaskGid(file)
|
||||
|
||||
let downloadurl = ''
|
||||
let crc64 = ''
|
||||
const downitem: IStateDownFile = {
|
||||
DownID: userID + '|' + file.file_id,
|
||||
Info: {
|
||||
GID: file.file_id.toLowerCase().substring(file.file_id.length - 16 + sizehex.length) + sizehex,
|
||||
GID: gid,
|
||||
user_id: userID,
|
||||
DownSavePath: fullPath,
|
||||
ariaRemote: ariaRemote,
|
||||
@@ -282,6 +292,8 @@ export default class DownDAL {
|
||||
} else {
|
||||
useFootStore().mSaveDownTotalSpeedInfo('')
|
||||
}
|
||||
await DownDAL.aCloud123OfflineProgress()
|
||||
|
||||
downingStore.mRefreshListDataShow(true)
|
||||
downedStore.mRefreshListDataShow(true)
|
||||
}
|
||||
@@ -387,6 +399,7 @@ export default class DownDAL {
|
||||
// 删除临时文件
|
||||
for (let downFile of deleteList) {
|
||||
let downInfo = downFile.Info
|
||||
if (downInfo.offlineProvider === 'cloud123') continue
|
||||
if (downInfo.ariaRemote) continue
|
||||
try {
|
||||
if (!downInfo.isDir) {
|
||||
@@ -422,4 +435,101 @@ export default class DownDAL {
|
||||
static QueryIsDowning() {
|
||||
return useDowningStore().ListDataDowningCount > 0
|
||||
}
|
||||
}
|
||||
|
||||
static async aAddCloud123OfflineDownload(url: string, fileName: string, dirID: string | undefined) {
|
||||
const userID = useUserStore().user_id
|
||||
if (!userID) return { success: false, message: '请先登录' }
|
||||
const { apiCloud123OfflineCreate } = await import('../cloud123/offline')
|
||||
const resp = await apiCloud123OfflineCreate(userID, url, fileName, dirID)
|
||||
if (!resp.taskId) return { success: false, message: resp.error || '创建离线下载失败' }
|
||||
const downitem: IStateDownFile = {
|
||||
DownID: `${userID}|cloud123_offline_${resp.taskId}`,
|
||||
Info: {
|
||||
GID: `cloud123_offline_${resp.taskId}`,
|
||||
user_id: userID,
|
||||
DownSavePath: '',
|
||||
ariaRemote: false,
|
||||
file_id: '',
|
||||
drive_id: 'cloud123',
|
||||
name: fileName || url,
|
||||
size: 0,
|
||||
sizestr: '',
|
||||
icon: 'iconcloud-download',
|
||||
isDir: false,
|
||||
encType: '',
|
||||
sha1: '',
|
||||
crc64: '',
|
||||
offlineProvider: 'cloud123',
|
||||
offlineTaskId: String(resp.taskId),
|
||||
offlineDirId: dirID || ''
|
||||
},
|
||||
Down: {
|
||||
DownState: '离线下载中',
|
||||
DownTime: Date.now(),
|
||||
DownSize: 0,
|
||||
DownSpeed: 0,
|
||||
DownSpeedStr: '',
|
||||
DownProcess: 0,
|
||||
IsStop: false,
|
||||
IsDowning: true,
|
||||
IsCompleted: false,
|
||||
IsFailed: false,
|
||||
FailedCode: 0,
|
||||
FailedMessage: '',
|
||||
AutoTry: 0,
|
||||
DownUrl: url
|
||||
}
|
||||
}
|
||||
useDowningStore().mAddDownload({ downlist: [downitem] })
|
||||
return { success: true, message: '' }
|
||||
}
|
||||
|
||||
private static cloud123OfflineTick = 0
|
||||
|
||||
static async aCloud123OfflineProgress() {
|
||||
const downingStore = useDowningStore()
|
||||
const list = downingStore.ListDataRaw
|
||||
if (!list.length) return
|
||||
DownDAL.cloud123OfflineTick = (DownDAL.cloud123OfflineTick + 1) % 5
|
||||
if (DownDAL.cloud123OfflineTick !== 0) return
|
||||
const { apiCloud123OfflineProcess } = await import('../cloud123/offline')
|
||||
const saveList: IStateDownFile[] = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
if (item.Info.offlineProvider !== 'cloud123' || !item.Info.offlineTaskId) continue
|
||||
if (item.Down.IsCompleted || item.Down.IsFailed) continue
|
||||
const info = await apiCloud123OfflineProcess(item.Info.user_id, item.Info.offlineTaskId)
|
||||
if (info.error) {
|
||||
item.Down.IsFailed = true
|
||||
item.Down.IsDowning = false
|
||||
item.Down.DownState = '离线下载失败'
|
||||
item.Down.FailedMessage = info.error
|
||||
saveList.push(item)
|
||||
continue
|
||||
}
|
||||
const process = Math.max(0, Math.min(100, info.process))
|
||||
item.Down.DownProcess = process
|
||||
item.Down.DownSpeedStr = ''
|
||||
if (info.status === 2) {
|
||||
item.Down.IsCompleted = true
|
||||
item.Down.IsDowning = false
|
||||
item.Down.DownState = '离线下载完成'
|
||||
item.Down.DownProcess = 100
|
||||
} else if (info.status === 1) {
|
||||
item.Down.IsFailed = true
|
||||
item.Down.IsDowning = false
|
||||
item.Down.DownState = '离线下载失败'
|
||||
} else if (info.status === 3) {
|
||||
item.Down.IsDowning = true
|
||||
item.Down.DownState = `离线下载重试中 ${process}%`
|
||||
} else {
|
||||
item.Down.IsDowning = true
|
||||
item.Down.DownState = `离线下载中 ${process}%`
|
||||
}
|
||||
saveList.push(item)
|
||||
}
|
||||
if (saveList.length) {
|
||||
DBDown.saveDownings(JSON.parse(JSON.stringify(saveList)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
useDowningStore,
|
||||
useKeyboardStore,
|
||||
useMouseStore,
|
||||
useUserStore,
|
||||
useUploadingStore,
|
||||
useWinStore
|
||||
} from '../store'
|
||||
import { isCloud123User } from '../aliapi/utils'
|
||||
import {
|
||||
onHideRightMenuScroll,
|
||||
onShowRightMenu,
|
||||
@@ -23,6 +25,7 @@ import {
|
||||
import { Tooltip as AntdTooltip } from 'ant-design-vue'
|
||||
import { TestButton } from '../utils/mosehelper'
|
||||
import { xorWith } from 'lodash'
|
||||
import { modalCloud123OfflineDownload } from '../utils/modal'
|
||||
|
||||
const viewlist = ref()
|
||||
const inputsearch = ref()
|
||||
@@ -30,6 +33,8 @@ const inputsearch = ref()
|
||||
const appStore = useAppStore()
|
||||
const winStore = useWinStore()
|
||||
const downingStore = useDowningStore()
|
||||
const userStore = useUserStore()
|
||||
const isCloudUser = computed(() => isCloud123User(userStore.user_id || ''))
|
||||
|
||||
const isDowning = computed(() => downingStore.ListDataDowningCount > 0)
|
||||
watch(isDowning, (value, oldValue) => {
|
||||
@@ -205,6 +210,11 @@ const handleRightClick = (e: { event: MouseEvent; node: any }) => {
|
||||
if (!downingStore.ListSelected.has(key)) downingStore.mMouseSelect(key, false, false)
|
||||
onShowRightMenu('downingrightmenu', e.event.clientX, e.event.clientY)
|
||||
}
|
||||
|
||||
const handleCloud123Offline = () => {
|
||||
if (!isCloudUser.value) return
|
||||
modalCloud123OfflineDownload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -247,6 +257,9 @@ const handleRightClick = (e: { event: MouseEvent; node: any }) => {
|
||||
</a-button>
|
||||
<a-button type='text' size='small' tabindex='-1' @click='handleDeleteAll'><i class='iconfont icondelete' />删除全部
|
||||
</a-button>
|
||||
<a-button v-if='isCloudUser' type='text' size='small' tabindex='-1' @click='handleCloud123Offline'>
|
||||
<i class='iconfont iconcloud-download' />离线下载
|
||||
</a-button>
|
||||
</div>
|
||||
<div class='toppanbtn' v-show='!downingStore.IsListSelected'>
|
||||
<a-button type='text' size='small' tabindex='-1' @click='handleStartAll'><i class='iconfont iconstart' />开始全部
|
||||
@@ -255,6 +268,9 @@ const handleRightClick = (e: { event: MouseEvent; node: any }) => {
|
||||
</a-button>
|
||||
<a-button type='text' size='small' tabindex='-1' @click='handleDeleteAll'><i class='iconfont icondelete' />删除全部
|
||||
</a-button>
|
||||
<a-button v-if='isCloudUser' type='text' size='small' tabindex='-1' @click='handleCloud123Offline'>
|
||||
<i class='iconfont iconcloud-download' />离线下载
|
||||
</a-button>
|
||||
</div>
|
||||
<div style='flex-grow: 1'></div>
|
||||
<div class='toppanbtn'>
|
||||
|
||||
@@ -252,6 +252,7 @@ const useDowningStore = defineStore('downing', {
|
||||
const DowningList = this.ListDataRaw
|
||||
for (const downID of this.ListSelected) {
|
||||
const selectedDown: IStateDownFile | undefined = DowningList.find(down => down.DownID === downID)
|
||||
if (selectedDown?.Info.offlineProvider === 'cloud123') continue
|
||||
if (selectedDown && !selectedDown.Down.IsDowning && !selectedDown.Down.IsCompleted) {
|
||||
this.mUpdateDownState(selectedDown, 'queue')
|
||||
}
|
||||
@@ -265,6 +266,7 @@ const useDowningStore = defineStore('downing', {
|
||||
const DowningList = this.ListDataRaw
|
||||
for (let j = 0; j < DowningList.length; j++) {
|
||||
const down = DowningList[j].Down
|
||||
if (DowningList[j].Info.offlineProvider === 'cloud123') continue
|
||||
if (down.IsDowning || down.IsCompleted) continue
|
||||
this.mUpdateDownState(DowningList[j], 'queue')
|
||||
}
|
||||
@@ -282,9 +284,11 @@ const useDowningStore = defineStore('downing', {
|
||||
if (DowningList[j].DownID == DownID) {
|
||||
const down = DowningList[j].Down
|
||||
if (down.IsCompleted) continue
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
downList.push(DowningList[j])
|
||||
this.mUpdateDownState(DowningList[j], 'stop')
|
||||
if (DowningList[j].Info.offlineProvider !== 'cloud123') {
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
downList.push(DowningList[j])
|
||||
this.mUpdateDownState(DowningList[j], 'stop')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -302,8 +306,10 @@ const useDowningStore = defineStore('downing', {
|
||||
for (let j = 0; j < DowningList.length; j++) {
|
||||
const down = DowningList[j].Down
|
||||
if (down.IsCompleted) continue
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
this.mUpdateDownState(DowningList[j], 'stop')
|
||||
if (DowningList[j].Info.offlineProvider !== 'cloud123') {
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
this.mUpdateDownState(DowningList[j], 'stop')
|
||||
}
|
||||
}
|
||||
await DownDAL.stopDowning(DowningList, gidList)
|
||||
this.mRefreshListDataShow(true)
|
||||
@@ -324,7 +330,9 @@ const useDowningStore = defineStore('downing', {
|
||||
const DownID = DowningList[j].DownID
|
||||
if (downIDList.includes(DownID)) {
|
||||
DowningList[j].Down.DownState = '待删除'
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
if (DowningList[j].Info.offlineProvider !== 'cloud123') {
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
}
|
||||
deleteList.push(DowningList[j])
|
||||
if (newListSelected.has(DownID)) {
|
||||
newListSelected.delete(DownID)
|
||||
@@ -348,7 +356,9 @@ const useDowningStore = defineStore('downing', {
|
||||
const DowningList = this.ListDataRaw
|
||||
for (let j = 0; j < DowningList.length; j++) {
|
||||
DowningList[j].Down.DownState = '待删除'
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
if (DowningList[j].Info.offlineProvider !== 'cloud123') {
|
||||
gidList.push(DowningList[j].Info.GID)
|
||||
}
|
||||
}
|
||||
await DownDAL.deleteDowning(true, DowningList, gidList)
|
||||
DowningList.splice(0, DowningList.length)
|
||||
|
||||
@@ -30,10 +30,12 @@ import PasswordModal from '../pan/topbtns/PasswordModal.vue'
|
||||
import UpdateModal from '../pan/topbtns/ShowUpdateModal.vue'
|
||||
import ShowUpdateModal from '../pan/topbtns/ShowUpdateModal.vue'
|
||||
import SelectVideoQualityModal from '../pan/topbtns/SelectVideoQualityModal.vue'
|
||||
import Cloud123OfflineDownloadModal from '../down/Cloud123OfflineDownloadModal.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectVideoQualityModal,
|
||||
Cloud123OfflineDownloadModal,
|
||||
ShowUpdateModal,
|
||||
UpdateModal,
|
||||
PasswordModal,
|
||||
@@ -158,6 +160,8 @@ export default defineComponent({
|
||||
:quality-data="modalStore.modalData.qualityData || {}"
|
||||
:callback="modalStore.modalData.callback" />
|
||||
|
||||
<Cloud123OfflineDownloadModal :visible="modalStore.modalName == 'cloud123offline'" />
|
||||
|
||||
<PostModal :visible="modalStore.modalName == 'showpostmodal'"
|
||||
:msg='modalStore.modalData.msg || ""'
|
||||
:msgid='modalStore.modalData.msgid || ""' />
|
||||
|
||||
@@ -21,6 +21,7 @@ import Rss from '../rss/index.vue'
|
||||
import Share from '../share/index.vue'
|
||||
import Down from '../down/index.vue'
|
||||
import Pan from '../pan/index.vue'
|
||||
import MediaLibraryView from '../views/MediaLibraryView.vue'
|
||||
|
||||
import UserInfo from '../user/UserInfo.vue'
|
||||
import UserLogin from '../user/UserLogin.vue'
|
||||
@@ -31,6 +32,7 @@ import { B64decode } from '../utils/format'
|
||||
import { throttle } from '../utils/debounce'
|
||||
|
||||
const panVisible = ref(true)
|
||||
const mediaNavVisible = ref(true)
|
||||
const appStore = useAppStore()
|
||||
const winStore = useWinStore()
|
||||
const keyboardStore = useKeyboardStore()
|
||||
@@ -41,6 +43,10 @@ const handlePanVisible = () => {
|
||||
panVisible.value = !panVisible.value
|
||||
}
|
||||
|
||||
const handleMediaNavVisible = () => {
|
||||
mediaNavVisible.value = !mediaNavVisible.value
|
||||
}
|
||||
|
||||
const handleThemeClick = (val: any) => {
|
||||
if (appStore.appTheme == 'system') {
|
||||
if (appStore.appDark) {
|
||||
@@ -84,7 +90,8 @@ keyboardStore.$subscribe((_m: any, state: KeyboardState) => {
|
||||
if (TestAlt('2', state.KeyDownEvent, () => appStore.toggleTab('down'))) return
|
||||
if (TestAlt('3', state.KeyDownEvent, () => appStore.toggleTab('share'))) return
|
||||
if (TestAlt('4', state.KeyDownEvent, () => appStore.toggleTab('rss'))) return
|
||||
if (TestAlt('5', state.KeyDownEvent, () => appStore.toggleTab('setting'))) return
|
||||
if (TestAlt('5', state.KeyDownEvent, () => appStore.toggleTab('media'))) return
|
||||
if (TestAlt('6', state.KeyDownEvent, () => appStore.toggleTab('setting'))) return
|
||||
if (TestAlt('f4', state.KeyDownEvent, () => handleHideClick(undefined))) return
|
||||
if (TestAlt('m', state.KeyDownEvent, () => handleMinClick(undefined))) return
|
||||
if (TestAlt('enter', state.KeyDownEvent, () => handleMaxClick(undefined))) return
|
||||
@@ -179,11 +186,16 @@ onUnmounted(() => {
|
||||
<i class='iconfont iconmenuon' v-if='panVisible' />
|
||||
<i class='iconfont iconmenuoff' v-else />
|
||||
</a-button>
|
||||
<div class='title'>阿里云盘</div>
|
||||
<a-button v-show="appStore.appTab === 'media'" type='text' size='small' @click='handleMediaNavVisible'>
|
||||
<i class='iconfont iconmenuon' v-if='mediaNavVisible' />
|
||||
<i class='iconfont iconmenuoff' v-else />
|
||||
</a-button>
|
||||
<div class='title'>小白羊 BoxPlayer</div>
|
||||
|
||||
<a-menu mode='horizontal' :selected-keys='[appStore.appTab]'
|
||||
@update:selected-keys='appStore.toggleTab($event[0])'>
|
||||
<a-menu-item key='pan' title='Alt+1'>网盘</a-menu-item>
|
||||
<a-menu-item key='media' title='Alt+5'>媒体库</a-menu-item>
|
||||
<a-menu-item key='down' title='Alt+2'>传输</a-menu-item>
|
||||
<a-menu-item key='share' title='Alt+3'>资源</a-menu-item>
|
||||
<a-menu-item key='rss' title='Alt+4'>插件</a-menu-item>
|
||||
@@ -198,7 +210,7 @@ onUnmounted(() => {
|
||||
v-if="appStore.appTheme === 'dark' || (appStore.appTheme == 'system' && appStore.appDark)"></i>
|
||||
<i class='iconfont iconday' v-else></i>
|
||||
</a-button>
|
||||
<a-button type='text' tabindex='-1' title='设置 Alt+6' :class="appStore.appTab == 'setting' ? 'active' : ''"
|
||||
<a-button type='text' tabindex='-1' title='设置 Alt+7' :class="appStore.appTab == 'setting' ? 'active' : ''"
|
||||
@click="appStore.toggleTab('setting')">
|
||||
<i class='iconfont iconsetting'></i>
|
||||
</a-button>
|
||||
@@ -227,7 +239,10 @@ onUnmounted(() => {
|
||||
<a-tab-pane key='rss' title='4'>
|
||||
<Rss />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key='setting' title='5'>
|
||||
<a-tab-pane key='media' title='5'>
|
||||
<MediaLibraryView :navVisible="mediaNavVisible" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key='setting' title='6'>
|
||||
<Setting />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang='ts'>
|
||||
import { KeyboardState, useAppStore, useKeyboardStore, useSettingStore } from '../store'
|
||||
import { useMediaLibraryStore } from '../store/medialibrary'
|
||||
import type { MediaLibraryItem } from '../types/media'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import Artplayer from 'artplayer'
|
||||
import HlsJs from 'hls.js'
|
||||
@@ -17,8 +19,16 @@ import artplayerPluginLibass from '../../src/module/video-plugins/artplayer-plug
|
||||
import PlayerUtils from '../utils/playerhelper'
|
||||
import { simpleToTradition, traditionToSimple } from 'chinese-simple2traditional'
|
||||
import path from 'path'
|
||||
import UserDAL from '../user/userdal'
|
||||
import Config from '../config'
|
||||
import { isBaiduUser, isCloud123User, isDrive115User } from '../aliapi/utils'
|
||||
import { apiCloud123FileList, mapCloud123FileToAliModel } from '../cloud123/dirfilelist'
|
||||
import { apiDrive115FileList, mapDrive115FileToAliModel } from '../cloud115/dirfilelist'
|
||||
import { apiBaiduFileList, mapBaiduFileToAliModel } from '../cloudbaidu/dirfilelist'
|
||||
import { getWebDavConnection, getWebDavConnectionId, isWebDavDrive, listWebDavDirectory } from '../utils/webdavClient'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const mediaStore = useMediaLibraryStore()
|
||||
const pageVideo = appStore.pageVideo!
|
||||
const isTop = ref(false)
|
||||
let autoPlayNumber = 0
|
||||
@@ -157,7 +167,6 @@ const createVideo = async (name: string) => {
|
||||
Artplayer.LOG_VERSION = false
|
||||
Artplayer.PLAYBACK_RATE = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]
|
||||
ArtPlayerRef = new Artplayer(options)
|
||||
ArtPlayerRef.title = name
|
||||
// 获取用户配置
|
||||
initStorage(ArtPlayerRef)
|
||||
initEvent(ArtPlayerRef)
|
||||
@@ -173,39 +182,36 @@ const initStorage = (art: Artplayer) => {
|
||||
if (storage.get('autoSkipEnd') === undefined) storage.set('autoSkipEnd', 0)
|
||||
if (storage.get('longPressSpeed') === undefined) storage.set('longPressSpeed', 2)
|
||||
if (storage.get('autoSkipBegin') === undefined) storage.set('autoSkipBegin', 0)
|
||||
if (storage.get('videoVolume')) ArtPlayerRef.volume = parseFloat(storage.get('videoVolume'))
|
||||
if (storage.get('videoVolume')) ArtPlayerRef.volume = parseFloat(String(storage.get('videoVolume')))
|
||||
if (storage.get('videoMuted')) ArtPlayerRef.muted = storage.get('videoMuted') === 'true'
|
||||
}
|
||||
|
||||
// 自定义热键
|
||||
const initHotKey = (art: Artplayer) => {
|
||||
// enter
|
||||
art.hotkey.add(13, () => {
|
||||
art.hotkey.add("13", () => {
|
||||
art.fullscreen = !art.fullscreen
|
||||
})
|
||||
// z
|
||||
art.hotkey.add(90, () => {
|
||||
art.hotkey.add("90", () => {
|
||||
art.playbackRate = 1
|
||||
})
|
||||
// x
|
||||
art.hotkey.add(88, () => {
|
||||
art.playbackRate -= 0.5
|
||||
art.hotkey.add("88", () => {
|
||||
art.playbackRate = Math.max(0.5, art.playbackRate - 0.5)
|
||||
})
|
||||
// f
|
||||
art.hotkey.add(70, () => {
|
||||
art.hotkey.add("70", () => {
|
||||
art.fullscreen = !art.fullscreen
|
||||
})
|
||||
// m
|
||||
art.hotkey.add(77, () => {
|
||||
art.hotkey.add("77", () => {
|
||||
art.muted = !art.muted
|
||||
art.notice.show = art.muted ? '开启静音' : '关闭静音'
|
||||
})
|
||||
// c
|
||||
art.hotkey.add(67, () => {
|
||||
playbackRate += 0.5
|
||||
if (playbackRate > 4) {
|
||||
playbackRate = 4
|
||||
}
|
||||
art.hotkey.add("67", () => {
|
||||
playbackRate = Math.min(4, playbackRate + 0.5)
|
||||
art.playbackRate = playbackRate
|
||||
})
|
||||
art.events.proxy(document, 'keydown', (event: any) => {
|
||||
@@ -216,7 +222,7 @@ const initHotKey = (art: Artplayer) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
// 设置播放速度
|
||||
art.playbackRate = art.storage.get('longPressSpeed') || 2
|
||||
art.playbackRate = Number(art.storage.get('longPressSpeed')) || 2
|
||||
art.notice.show = `x${art.playbackRate.toFixed(1)} 倍速播放中`
|
||||
} else {
|
||||
longPressSpeed = art.playbackRate
|
||||
@@ -275,19 +281,21 @@ const initEvent = (art: Artplayer) => {
|
||||
// 播放倍数变化
|
||||
art.on('video:ratechange', () => {
|
||||
playbackRate = art.playbackRate
|
||||
let $panel = art.query('.art-setting-panel.art-current')
|
||||
let $tooltip = art.query('.art-current .art-setting-item-right-tooltip')
|
||||
if ($tooltip) $tooltip.innerText = playbackRate === 1.0 ? art.i18n.get('Normal') : playbackRate.toFixed(1)
|
||||
const $current = Artplayer.utils.queryAll('.art-setting-item', $panel)
|
||||
.find((item) => Number(item.dataset.value) === playbackRate)
|
||||
if ($current) Artplayer.utils.inverseClass($current, 'art-current')
|
||||
const $panel = art.query('.art-setting-panel.art-current')
|
||||
const $tooltip = art.query('.art-current .art-setting-item-right-tooltip')
|
||||
if ($tooltip) ($tooltip as HTMLElement).innerText = playbackRate === 1.0 ? art.i18n.get('Normal') : playbackRate.toFixed(1)
|
||||
if ($panel) {
|
||||
const $current = Artplayer.utils.queryAll('.art-setting-item', $panel as HTMLElement)
|
||||
.find((item) => Number((item as HTMLElement).dataset.value) === playbackRate)
|
||||
if ($current) Artplayer.utils.inverseClass($current as HTMLElement, 'art-current')
|
||||
}
|
||||
})
|
||||
// 播放时间变化
|
||||
art.on('video:timeupdate', async () => {
|
||||
if (art.video.currentTime > 0
|
||||
&& !art.video.paused && !art.video.ended
|
||||
&& art.video.readyState > art.video.HAVE_CURRENT_DATA) {
|
||||
const endDuration = art.storage.get('autoSkipEnd')
|
||||
const endDuration = art.storage.get('autoSkipEnd') as number
|
||||
const currentTime = art.currentTime
|
||||
if (currentTime > 0 && endDuration > 0) {
|
||||
if (endDuration <= currentTime) {
|
||||
@@ -313,9 +321,9 @@ const jumpToNextVideo = async (art: Artplayer) => {
|
||||
await refreshSetting(art, item)
|
||||
await refreshPlayList(art, item.file_id)
|
||||
// 重新载入弹幕
|
||||
if (!art.plugins.artplayerPluginDanmuku.isHide) {
|
||||
await art.plugins.artplayerPluginDanmuku.stop()
|
||||
await art.plugins.artplayerPluginDanmuku.load()
|
||||
if (!(art.plugins.artplayerPluginDanmaku as any)?.isHide) {
|
||||
await (art.plugins.artplayerPluginDanmaku as any)?.stop()
|
||||
await (art.plugins.artplayerPluginDanmaku as any)?.load()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,22 +331,38 @@ const curDirFileList: any[] = []
|
||||
const childDirFileList: any[] = []
|
||||
const getDirFileList = async (dir_id: string, hasDir: boolean, category: string = '', filter?: RegExp): Promise<any[]> => {
|
||||
if (curDirFileList.length === 0 || (hasDir && childDirFileList.length === 0)) {
|
||||
const dir = await AliDirFileList.ApiDirFileList(pageVideo.user_id, pageVideo.drive_id, dir_id, '', 'name asc', '')
|
||||
if (!dir.next_marker) {
|
||||
for (let item of dir.items) {
|
||||
const fileInfo = {
|
||||
html: item.name,
|
||||
category: item.category,
|
||||
description: item.description,
|
||||
name: item.name,
|
||||
file_id: item.file_id,
|
||||
ext: item.ext,
|
||||
isDir: item.isDir,
|
||||
encType: getEncType({ description: item.description })
|
||||
}
|
||||
if (!hasDir) curDirFileList.push(fileInfo)
|
||||
else childDirFileList.push(fileInfo)
|
||||
let items: any[] = []
|
||||
if (isWebDavDrive(pageVideo.drive_id)) {
|
||||
const connection = getWebDavConnection(getWebDavConnectionId(pageVideo.drive_id))
|
||||
if (connection) {
|
||||
items = await listWebDavDirectory(connection, dir_id || '/')
|
||||
}
|
||||
} else if (isCloud123User(pageVideo.user_id) || pageVideo.drive_id === 'cloud123') {
|
||||
const list = await apiCloud123FileList(pageVideo.user_id, dir_id, 500, false)
|
||||
items = list.map(item => mapCloud123FileToAliModel(item))
|
||||
} else if (isDrive115User(pageVideo.user_id) || pageVideo.drive_id === 'drive115') {
|
||||
const list = await apiDrive115FileList(pageVideo.user_id, dir_id, 200, 0, true)
|
||||
items = list.map(item => mapDrive115FileToAliModel(item, pageVideo.drive_id))
|
||||
} else if (isBaiduUser(pageVideo.user_id) || pageVideo.drive_id === 'baidu') {
|
||||
const list = await apiBaiduFileList(pageVideo.user_id, dir_id || '/', 'name', 0, 1000)
|
||||
items = list.map(item => mapBaiduFileToAliModel(item, pageVideo.drive_id, dir_id || '/'))
|
||||
} else {
|
||||
const dir = await AliDirFileList.ApiDirFileList(pageVideo.user_id, pageVideo.drive_id, dir_id, '', 'name asc', '')
|
||||
if (!dir.next_marker) items = dir.items
|
||||
}
|
||||
for (let item of items) {
|
||||
const fileInfo = {
|
||||
html: item.name,
|
||||
category: item.category,
|
||||
description: item.description,
|
||||
name: item.name,
|
||||
file_id: item.file_id,
|
||||
ext: item.ext,
|
||||
isDir: item.isDir,
|
||||
encType: getEncType({ description: item.description || '' })
|
||||
}
|
||||
if (!hasDir) curDirFileList.push(fileInfo)
|
||||
else childDirFileList.push(fileInfo)
|
||||
}
|
||||
}
|
||||
const filterList = hasDir ? [...childDirFileList, ...curDirFileList].sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')) : curDirFileList
|
||||
@@ -377,11 +401,11 @@ const refreshSetting = async (art: Artplayer, item: any) => {
|
||||
}
|
||||
|
||||
const defaultSettings = async (art: Artplayer) => {
|
||||
let autoPlayNext = art.storage.get('autoPlayNext')
|
||||
let autoJumpCursor = art.storage.get('autoJumpCursor')
|
||||
let autoSkipBegin = art.storage.get('autoSkipBegin')
|
||||
let autoSkipEnd = art.storage.get('autoSkipEnd')
|
||||
let longPressSpeed = art.storage.get('longPressSpeed')
|
||||
let autoPlayNext = art.storage.get('autoPlayNext') as boolean
|
||||
let autoJumpCursor = art.storage.get('autoJumpCursor') as boolean
|
||||
let autoSkipBegin = art.storage.get('autoSkipBegin') as number
|
||||
let autoSkipEnd = art.storage.get('autoSkipEnd') as number
|
||||
let longPressSpeed = art.storage.get('longPressSpeed') as number
|
||||
art.setting.update({
|
||||
name: 'autoJumpCursor',
|
||||
width: 300,
|
||||
@@ -470,7 +494,7 @@ const defaultControls = async (art: Artplayer) => {
|
||||
html: '片头',
|
||||
tooltip: '点击设置片头',
|
||||
click: async (component, event) => {
|
||||
if (art.storage.get('autoSkipBegin') > 0) {
|
||||
if (Number(art.storage.get('autoSkipBegin')) > 0) {
|
||||
art.storage.set('autoSkipBegin', 0)
|
||||
art.notice.show = `取消设置片头`
|
||||
} else {
|
||||
@@ -486,7 +510,7 @@ const defaultControls = async (art: Artplayer) => {
|
||||
html: '片尾',
|
||||
tooltip: '点击设置片尾',
|
||||
click: async (component, event) => {
|
||||
if (art.storage.get('autoSkipEnd') > 0) {
|
||||
if (Number(art.storage.get('autoSkipEnd')) > 0) {
|
||||
art.storage.set('autoSkipEnd', 0)
|
||||
art.notice.show = `取消设置片尾`
|
||||
} else {
|
||||
@@ -507,13 +531,22 @@ const loadPlugins = async (art: Artplayer) => {
|
||||
}
|
||||
|
||||
const getVideoInfo = async (art: Artplayer) => {
|
||||
if (pageVideo.drive_id === 'local') {
|
||||
const urlModule = window.require?.('url')
|
||||
const filePath = pageVideo.file_id || (pageVideo as any).file_path || ''
|
||||
if (filePath) {
|
||||
const fileUrl = urlModule?.pathToFileURL ? urlModule.pathToFileURL(filePath).href : `file://${encodeURI(filePath)}`
|
||||
art.url = fileUrl
|
||||
return
|
||||
}
|
||||
}
|
||||
// 获取视频链接
|
||||
const data: string | IRawUrl = await getRawUrl(pageVideo.user_id, pageVideo.drive_id, pageVideo.file_id, pageVideo.encType, pageVideo.password, false, 'video')
|
||||
if (typeof data != 'string' && data.qualities.length > 0) {
|
||||
// 画质
|
||||
let uiVideoQuality = useSettingStore().uiVideoQuality
|
||||
let defaultQuality: selectorItem
|
||||
if (uiVideoQuality === 'Origin') {
|
||||
if (uiVideoQuality === 'Origin' && pageVideo.drive_id != 'baidu' && pageVideo.drive_id != 'drive115') {
|
||||
// 代理播放
|
||||
data.qualities[0].url = getProxyUrl({
|
||||
user_id: pageVideo.user_id,
|
||||
@@ -540,14 +573,17 @@ const getVideoInfo = async (art: Artplayer) => {
|
||||
style: { marginRight: '15px' },
|
||||
html: defaultQuality ? defaultQuality.html : '',
|
||||
selector: data.qualities,
|
||||
onSelect: async (item: selectorItem) => {
|
||||
if (item.html === '原画' && art.hls) {
|
||||
art.hls.detachMedia()
|
||||
art.hls.destroy()
|
||||
delete art.hls
|
||||
onSelect: (selector: any, element: HTMLElement, event: Event) => {
|
||||
const item = selector as selectorItem
|
||||
if (item.html === '原画' && (art as any).hls) {
|
||||
(art as any).hls.detachMedia()
|
||||
(art as any).hls.destroy()
|
||||
delete (art as any).hls
|
||||
}
|
||||
await art.switchQuality(item.url)
|
||||
art.playbackRate = playbackRate
|
||||
art.switchQuality(item.url).then(() => {
|
||||
art.playbackRate = playbackRate
|
||||
})
|
||||
return item.html
|
||||
}
|
||||
})
|
||||
// 内嵌字幕
|
||||
@@ -574,6 +610,7 @@ const getVideoInfo = async (art: Artplayer) => {
|
||||
|
||||
let playList: selectorItem[] = []
|
||||
const refreshPlayList = async (art: Artplayer, file_id?: string) => {
|
||||
if (pageVideo.drive_id === 'local') return
|
||||
if (!file_id) {
|
||||
let fileList: any = await getDirFileList(pageVideo.parent_file_id, false, 'video') || []
|
||||
if (fileList && fileList.length > 1) {
|
||||
@@ -614,21 +651,26 @@ const refreshPlayList = async (art: Artplayer, file_id?: string) => {
|
||||
style: { padding: '0 10px', marginRight: '10px' },
|
||||
html: handlerPlayTitle(curPlayTitle),
|
||||
selector: playList,
|
||||
mounted: (panel: HTMLDivElement) => {
|
||||
mounted: (element: HTMLElement) => {
|
||||
const panel = element as HTMLDivElement
|
||||
const $current = Artplayer.utils.queryAll('.art-selector-item', panel)
|
||||
.find((item) => Number(item.dataset.index) == autoPlayNumber)
|
||||
$current && Artplayer.utils.addClass($current, 'art-list-icon')
|
||||
.find((item) => Number((item as HTMLElement).dataset.index) == autoPlayNumber)
|
||||
if ($current) Artplayer.utils.addClass($current as HTMLElement, 'art-list-icon')
|
||||
},
|
||||
onSelect: async (item: SettingOption, element: HTMLElement) => {
|
||||
await updateVideoTime()
|
||||
await refreshSetting(art, item)
|
||||
lastPlayNumber = autoPlayNumber - 1
|
||||
Artplayer.utils.inverseClass(element, 'art-list-icon')
|
||||
// 重新载入弹幕
|
||||
if (!art.plugins.artplayerPluginDanmuku.isHide) {
|
||||
await art.plugins.artplayerPluginDanmuku.stop()
|
||||
await art.plugins.artplayerPluginDanmuku.load()
|
||||
}
|
||||
onSelect: (selector: any, element: HTMLElement, event: Event) => {
|
||||
const item = selector as selectorItem
|
||||
updateVideoTime().then(() => {
|
||||
refreshSetting(art, item).then(() => {
|
||||
lastPlayNumber = autoPlayNumber - 1
|
||||
Artplayer.utils.inverseClass(element, 'art-list-icon')
|
||||
// 重新载入弹幕
|
||||
if (!(art.plugins.artplayerPluginDanmaku as any)?.isHide) {
|
||||
(art.plugins.artplayerPluginDanmaku as any)?.stop?.().then(() => {
|
||||
(art.plugins.artplayerPluginDanmaku as any)?.load?.()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return handlerPlayTitle(item.html)
|
||||
}
|
||||
})
|
||||
@@ -640,18 +682,18 @@ const handlerPlayTitle = (html: string) => {
|
||||
}
|
||||
|
||||
const getVideoCursor = async (art: Artplayer, play_cursor?: number) => {
|
||||
const autoSkipBegin = art.storage.get('autoSkipBegin')
|
||||
const autoSkipBegin = art.storage.get('autoSkipBegin') as number
|
||||
if (art.storage.get('autoJumpCursor')) {
|
||||
let cursor = 0
|
||||
// 进度
|
||||
if (play_cursor) {
|
||||
cursor = play_cursor
|
||||
} else {
|
||||
const info = await AliFile.ApiFileInfo(pageVideo.user_id, pageVideo.drive_id, pageVideo.file_id)
|
||||
const info = await PlayerUtils.getPlayCursor(pageVideo.user_id, pageVideo.drive_id, pageVideo.file_id)
|
||||
if (info?.play_cursor) {
|
||||
cursor = info?.play_cursor
|
||||
} else if (info?.user_meta) {
|
||||
const meta = JSON.parse(info?.user_meta)
|
||||
cursor = info.play_cursor as number
|
||||
} else if (info?.info?.user_meta) {
|
||||
const meta = JSON.parse(info?.info?.user_meta)
|
||||
if (meta.play_cursor) {
|
||||
cursor = parseFloat(meta.play_cursor)
|
||||
}
|
||||
@@ -707,7 +749,7 @@ let embedSubSelector: selectorItem[] = []
|
||||
const getSubTitleList = async (art: Artplayer) => {
|
||||
// 尝试加载当前文件夹字幕文件
|
||||
let subSelector: selectorItem[]
|
||||
const hasDir = art.storage.get('subTitleListMode')
|
||||
const hasDir = art.storage.get('subTitleListMode') as boolean
|
||||
// 加载二级目录(仅加载一个文件夹)
|
||||
let file_id = ''
|
||||
if (hasDir) {
|
||||
@@ -753,7 +795,6 @@ const getSubTitleList = async (art: Artplayer) => {
|
||||
// 字幕设置面板
|
||||
art.setting.update({
|
||||
name: 'Subtitle',
|
||||
width: 300,
|
||||
html: '字幕设置',
|
||||
tooltip: art.subtitle.show ? (subDefault.url !== '' ? '字幕开启' : subDefault.html) : '字幕关闭',
|
||||
selector: [{
|
||||
@@ -770,14 +811,15 @@ const getSubTitleList = async (art: Artplayer) => {
|
||||
art.subtitle.style('fontSize', subtitleSize)
|
||||
let currentItem = Artplayer.utils.queryAll('.art-setting-panel.art-current .art-setting-item:nth-of-type(n+3)')
|
||||
if (currentItem.length > 0) {
|
||||
currentItem.forEach((current: HTMLElement) => {
|
||||
currentItem.forEach((current: Element) => {
|
||||
const currentElement = current as HTMLElement
|
||||
if (item.switch) {
|
||||
!art.subtitle.url && Artplayer.utils.removeClass(current, 'art-current')
|
||||
Artplayer.utils.addClass(current, 'disable')
|
||||
!art.subtitle.url && Artplayer.utils.removeClass(currentElement, 'art-current')
|
||||
Artplayer.utils.addClass(currentElement, 'disable')
|
||||
item.$parentItem.tooltip = subDefault.url !== '' ? '字幕开启' : subDefault.html
|
||||
} else {
|
||||
item.$parentItem.tooltip = '字幕开启'
|
||||
Artplayer.utils.removeClass(current, 'disable')
|
||||
Artplayer.utils.removeClass(currentElement, 'disable')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -859,14 +901,18 @@ const getSubTitleList = async (art: Artplayer) => {
|
||||
return size
|
||||
}
|
||||
}, ...subSelector],
|
||||
onSelect: async (item: SettingOption, element: HTMLDivElement) => {
|
||||
onSelect: (selector: any, element: HTMLElement, event: Event) => {
|
||||
const item = selector as selectorItem
|
||||
if (art.subtitle.show) {
|
||||
if (!item.file_id) {
|
||||
art.notice.show = ''
|
||||
await art.subtitle.switch(item.url, { name: item.name, escape: false })
|
||||
return item.html
|
||||
art.subtitle.switch(item.url, { name: item.name, escape: false }).then(() => {
|
||||
return item.html
|
||||
})
|
||||
} else {
|
||||
return await loadOnlineSub(art, item)
|
||||
loadOnlineSub(art, item).then((result) => {
|
||||
return result
|
||||
})
|
||||
}
|
||||
} else {
|
||||
art.notice.show = '未开启字幕'
|
||||
@@ -884,6 +930,39 @@ const updateVideoTime = async () => {
|
||||
pageVideo.file_id,
|
||||
ArtPlayerRef.currentTime
|
||||
)
|
||||
updateContinueWatching()
|
||||
}
|
||||
|
||||
const findMediaItemByFileId = (fileId: string): MediaLibraryItem | undefined => {
|
||||
for (const item of mediaStore.mediaItems) {
|
||||
if (item.driveFiles?.some(file => file.id === fileId)) {
|
||||
return item
|
||||
}
|
||||
const season = item.seasons?.find(s =>
|
||||
s.episodes?.some(ep => ep.driveFiles?.some(file => file.id === fileId))
|
||||
)
|
||||
if (season) {
|
||||
const episode = season.episodes?.find(ep => ep.driveFiles?.some(file => file.id === fileId))
|
||||
if (episode) {
|
||||
return {
|
||||
...item,
|
||||
id: `${item.id}_${season.seasonNumber}_${episode.episodeNumber}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateContinueWatching = () => {
|
||||
const fileId = pageVideo.file_id
|
||||
const item = findMediaItemByFileId(fileId)
|
||||
if (!item) return
|
||||
const duration = ArtPlayerRef?.duration || 0
|
||||
const progress = duration > 0 ? ArtPlayerRef.currentTime / duration : 0
|
||||
item.watchProgress = Math.max(0, Math.min(1, progress))
|
||||
item.lastWatched = new Date()
|
||||
mediaStore.addToContinueWatching(item)
|
||||
}
|
||||
|
||||
const handleHideClick = async () => {
|
||||
|
||||
@@ -141,6 +141,11 @@ window.Electron.ipcRenderer.on('setTheme', (_event: any, args: any) => {
|
||||
const appStore = useAppStore()
|
||||
appStore.toggleDark(args.dark)
|
||||
})
|
||||
|
||||
window.Electron.ipcRenderer.on('cloud123-oauth-callback', (_event: any, url: string) => {
|
||||
if (!url) return
|
||||
window.dispatchEvent(new CustomEvent('cloud123-oauth-callback', { detail: url }))
|
||||
})
|
||||
try {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
} catch {}
|
||||
@@ -149,4 +154,3 @@ try {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class MixBase64 {
|
||||
static sourceChars = source.split('')
|
||||
|
||||
encode(bufferOrStr: Buffer | string, encoding: BufferEncoding = 'utf-8'): string {
|
||||
const buffer = bufferOrStr instanceof Buffer ? bufferOrStr : Buffer.from(bufferOrStr, encoding)
|
||||
const buffer = typeof bufferOrStr === 'string' ? Buffer.from(bufferOrStr, encoding) : Buffer.from(bufferOrStr)
|
||||
let result = ''
|
||||
let arr: any = []
|
||||
let bt: any = []
|
||||
|
||||
@@ -6,13 +6,14 @@ import usePanTreeStore, { PanTreeState } from './pantreestore'
|
||||
import MySwitchTab from '../layout/MySwitchTab.vue'
|
||||
import { KeyboardState, useAppStore, useKeyboardStore, usePanFileStore, useSettingStore, useWinStore } from '../store'
|
||||
import PanDAL from './pandal'
|
||||
import UserDAL from '../user/userdal'
|
||||
import { onHideRightMenuScroll, onShowRightMenu, TestCtrl } from '../utils/keyboardhelper'
|
||||
import DirLeftMenu from './menus/DirLeftMenu.vue'
|
||||
import { TreeNodeData } from '../store/treestore'
|
||||
import { dropMoveSelectedFile } from './topbtns/topbtn'
|
||||
import message from '../utils/message'
|
||||
import { modalUpload } from '../utils/modal'
|
||||
import { GetDriveType } from '../aliapi/utils'
|
||||
import { GetDriveType, isAliyunUser, isBaiduUser, isCloud123User, isDrive115User } from '../aliapi/utils'
|
||||
|
||||
const treeref = ref()
|
||||
const inputselectType = ref('backup')
|
||||
@@ -22,6 +23,8 @@ const quickHeight = computed(() => winStore.height - 42 - 56 - 24 - 4 - 280 - 28
|
||||
const appStore = useAppStore()
|
||||
const pantreeStore = usePanTreeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const isCloudUser = computed(() => isCloud123User(pantreeStore.user_id || ''))
|
||||
const isAliyunAccount = computed(() => isAliyunUser(pantreeStore.user_id || UserDAL.GetUserToken(pantreeStore.user_id || '')))
|
||||
|
||||
const keyboardStore = useKeyboardStore()
|
||||
keyboardStore.$subscribe((_m: any, state: KeyboardState) => {
|
||||
@@ -76,7 +79,9 @@ watchEffect(() => {
|
||||
|
||||
const handleTreeRightClick = (e: { event: MouseEvent; node: any }) => {
|
||||
const { parent = undefined, key } = e.node
|
||||
if (key.length < 40 || key.startsWith('search')) return
|
||||
if (key.startsWith('search')) return
|
||||
const isSingleRootDrive = isCloud123User(pantreeStore.user_id || '') || isDrive115User(pantreeStore.user_id || '') || isBaiduUser(pantreeStore.user_id || '')
|
||||
if (!isSingleRootDrive && key.length < 40) return
|
||||
pantreeStore.mTreeSelected(e)
|
||||
onShowRightMenu('leftpanmenu', e.event.clientX, e.event.clientY)
|
||||
}
|
||||
@@ -153,21 +158,43 @@ const handleQuickSelect = (index: number) => {
|
||||
}
|
||||
}
|
||||
const filterTreeData = computed(() => {
|
||||
return pantreeStore.treeData.filter((item) => {
|
||||
if (useSettingStore().securityHideBackupDrive && item.key === 'backup_root') {
|
||||
return false
|
||||
}
|
||||
if (useSettingStore().securityHideResourceDrive && item.key === 'resource_root') {
|
||||
return false
|
||||
}
|
||||
if (useSettingStore().securityHidePicDrive && item.key === 'pic_root') {
|
||||
return false
|
||||
}
|
||||
if (!usePanTreeStore().resource_drive_id && item.key === 'resource_root') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
const isCloudUser = isCloud123User(pantreeStore.user_id || '')
|
||||
const isWebDavNode = (item: any) => (item?.drive_id || '').startsWith('webdav:')
|
||||
const baseList = isCloudUser
|
||||
? pantreeStore.treeData.filter((item) => {
|
||||
if (isWebDavNode(item)) return false
|
||||
if (item.key === 'backup_root') return false
|
||||
if (item.key === 'resource_root') return false
|
||||
if (item.key === 'pic_root') return false
|
||||
return true
|
||||
})
|
||||
: pantreeStore.treeData.filter((item) => {
|
||||
if (isWebDavNode(item)) return false
|
||||
if (!isAliyunAccount.value && (item.key === 'backup_root' || item.key === 'resource_root')) {
|
||||
return false
|
||||
}
|
||||
if (isBaiduUser(pantreeStore.user_id || '') && item.key === 'trash') {
|
||||
return false
|
||||
}
|
||||
if (!isAliyunAccount.value && (item.key === 'pic_root' || item.key === 'video' || item.key === 'favorite' || item.key === 'recover')) {
|
||||
return false
|
||||
}
|
||||
if (useSettingStore().securityHideBackupDrive && item.key === 'backup_root') {
|
||||
return false
|
||||
}
|
||||
if (useSettingStore().securityHideResourceDrive && item.key === 'resource_root') {
|
||||
return false
|
||||
}
|
||||
if (useSettingStore().securityHidePicDrive && item.key === 'pic_root') {
|
||||
return false
|
||||
}
|
||||
if (!usePanTreeStore().resource_drive_id && item.key === 'resource_root') {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return baseList
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -213,7 +240,7 @@ const filterTreeData = computed(() => {
|
||||
<i class='iconfont iconfile-folder' />
|
||||
</template>
|
||||
<template #title='{ dataRef }'>
|
||||
<span v-if="dataRef.key.length == 40 || dataRef.key.includes('root')"
|
||||
<span v-if="String(dataRef.key).length == 40 || String(dataRef.key).includes('root')"
|
||||
class='dirtitle treedragnode'
|
||||
@drop='onRowItemDrop($event, dataRef)'
|
||||
@dragover='onRowItemDragOver'
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TestKeyboardScroll,
|
||||
TestKeyboardSelect
|
||||
} from '../utils/keyboardhelper'
|
||||
import { onMounted, ref, watchEffect } from 'vue'
|
||||
import { computed, onMounted, ref, watchEffect } from 'vue'
|
||||
import PanDAL from './pandal'
|
||||
|
||||
import { Tooltip as AntdTooltip } from 'ant-design-vue'
|
||||
@@ -48,6 +48,7 @@ import PanTopbtn from './menus/PanTopbtn.vue'
|
||||
import FileTopbtn from './menus/FileTopbtn.vue'
|
||||
import FileRightMenu from './menus/FileRightMenu.vue'
|
||||
import TrashRightMenu from './menus/TrashRightMenu.vue'
|
||||
import MediaLibraryMenu from './menus/MediaLibraryMenu.vue'
|
||||
import TrashTopbtn from './menus/TrashTopbtn.vue'
|
||||
import DirTopPath from './menus/DirTopPath.vue'
|
||||
import message from '../utils/message'
|
||||
@@ -55,7 +56,7 @@ import { menuOpenFile } from '../utils/openfile'
|
||||
import { throttle } from '../utils/debounce'
|
||||
import { TestButton } from '../utils/mosehelper'
|
||||
import usePanTreeStore from './pantreestore'
|
||||
import { GetDriveID, GetDriveType } from '../aliapi/utils'
|
||||
import { GetDriveID, GetDriveType, isAliyunUser, isCloud123User, isDrive115User } from '../aliapi/utils'
|
||||
import { xorWith } from 'lodash'
|
||||
|
||||
const viewlist = ref()
|
||||
@@ -65,6 +66,18 @@ const inputpicType = ref('pic_root')
|
||||
const inputselectType = ref('backup')
|
||||
const inputsearchType = ref<string[]>([])
|
||||
|
||||
const handleListScroll = (event: any) => {
|
||||
onHideRightMenuScroll()
|
||||
if (panfileStore.SelectDirType !== 'trash') return
|
||||
if (!isDrive115User(panTreeStore.user_id || '')) return
|
||||
const target = event?.target as HTMLElement | undefined
|
||||
if (!target) return
|
||||
const distance = target.scrollHeight - target.scrollTop - target.clientHeight
|
||||
if (distance < 120) {
|
||||
// PanDAL.LoadMoreDrive115Trash(panTreeStore.user_id, panfileStore.DriveID)
|
||||
}
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingStore = useSettingStore()
|
||||
const winStore = useWinStore()
|
||||
@@ -89,6 +102,9 @@ panfileStore.$subscribe((_m: any, state: PanFileState) => {
|
||||
if (menuShowVideo.value != isShowVideo) menuShowVideo.value = isShowVideo
|
||||
const isShowZip = !isTrash && panfileStore.ListSelected.size == 1 && (selectItem?.ext == 'zip' || selectItem?.ext == 'rar')
|
||||
if (menuShowZip.value != isShowZip) menuShowZip.value = isShowZip
|
||||
// 媒体库菜单:选中单个文件夹时显示
|
||||
const isShowMediaLibrary = !isTrash && panfileStore.ListSelected.size == 1 && (selectItem?.isDir ?? false)
|
||||
if (menuShowMediaLibrary.value != isShowMediaLibrary) menuShowMediaLibrary.value = isShowMediaLibrary
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -299,6 +315,7 @@ const handleSearchEnter = (event: any) => {
|
||||
|
||||
const menuShowVideo = ref(false)
|
||||
const menuShowZip = ref(false)
|
||||
const menuShowMediaLibrary = ref(false)
|
||||
const handleRightClick = (e: { event: MouseEvent; node: any }) => {
|
||||
const key = e.node.key
|
||||
if (!panfileStore.ListSelected.has(key)) panfileStore.mMouseSelect(key, false, false)
|
||||
@@ -575,7 +592,8 @@ const onPanDragEnd = (ev: any) => {
|
||||
<DirTopPath />
|
||||
<div style='flex-grow: 1'></div>
|
||||
<div v-if="panfileStore.SelectDirType == 'trash'" class='toppantip'>
|
||||
<span style='color: crimson'> 免费=10天 会员=30天 超级会员=60天</span>
|
||||
<span v-if="isCloud123User(panTreeStore.user_id || '')" style='color: crimson'>回收站内容保存30天,到期后自动清理,会员可延长期限至100天</span>
|
||||
<span v-else style='color: crimson'> 免费=10天 会员=30天 超级会员=60天</span>
|
||||
</div>
|
||||
<div v-if="panfileStore.SelectDirType == 'recover'" class='toppantip'>
|
||||
<span style='color: crimson'>仅会员可用 恢复60天内彻底删除的文件(不保留文件夹路径)</span>
|
||||
@@ -620,7 +638,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
<a-option value='mypic'>我的相册</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div v-show="!panfileStore.IsListSelected && ['trash', 'recover', 'favorite'].includes(panfileStore.SelectDirType)"
|
||||
<div v-show="!panfileStore.IsListSelected && ['trash', 'recover', 'favorite'].includes(panfileStore.SelectDirType) && isAliyunUser(panTreeStore.user_id || '')"
|
||||
class='toppanbtn'>
|
||||
<a-select v-model:model-value='inputselectType'
|
||||
size='small' tabindex='-1'
|
||||
@@ -632,7 +650,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
<a-option value='pic' :disabled="useSettingStore().securityHidePicDrive">相册</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div v-show="panfileStore.SelectDirType == 'search' && !panfileStore.IsListSelected" class='toppanbtn'>
|
||||
<div v-show="panfileStore.SelectDirType == 'search' && !panfileStore.IsListSelected && isAliyunUser(panTreeStore.user_id || '')" class='toppanbtn'>
|
||||
<a-dropdown style='width: 100px;' @popup-visible-change="handleSearchCheck">
|
||||
<a-button :disabled='panfileStore.ListLoading'>范围</a-button>
|
||||
<template #content>
|
||||
@@ -657,7 +675,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
@search='(val:string)=>topSearchAll(val, inputsearchType)'
|
||||
@press-enter='($event:any)=>topSearchAll($event.srcElement.value as string, inputsearchType)'
|
||||
@keydown.esc=';($event.target as any).blur()' />
|
||||
<a-button type='text' size='small' tabindex='-1' style='border: none'
|
||||
<a-button v-show=" isAliyunUser(panTreeStore.user_id || '')" type='text' size='small' tabindex='-1' style='border: none'
|
||||
@click="() => topSearchAll('topSearchAll高级搜索', inputsearchType)">高级
|
||||
</a-button>
|
||||
</div>
|
||||
@@ -818,7 +836,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
style='width: 100%'
|
||||
:data='panfileStore.ListDataShow'
|
||||
tabindex='-1'
|
||||
@scroll='onHideRightMenuScroll'>
|
||||
@scroll='handleListScroll'>
|
||||
<template #empty>
|
||||
<a-empty description='空文件夹' />
|
||||
</template>
|
||||
@@ -985,7 +1003,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
style='width: 100%'
|
||||
:data='panfileStore.ListDataGrid'
|
||||
tabindex='-1'
|
||||
@scroll='onHideRightMenuScroll'>
|
||||
@scroll='handleListScroll'>
|
||||
<template #empty>
|
||||
<a-empty description='空文件夹' />
|
||||
</template>
|
||||
@@ -1144,7 +1162,7 @@ const onPanDragEnd = (ev: any) => {
|
||||
style='width: 100%'
|
||||
:data='panfileStore.ListDataGrid'
|
||||
tabindex='-1'
|
||||
@scroll='onHideRightMenuScroll'>
|
||||
@scroll='handleListScroll'>
|
||||
<template #empty>
|
||||
<a-empty description='空文件夹' />
|
||||
</template>
|
||||
@@ -1291,6 +1309,11 @@ const onPanDragEnd = (ev: any) => {
|
||||
:inputpicType='inputpicType'
|
||||
:isallfavored='panfileStore.IsListSelectedFavAll' />
|
||||
<TrashRightMenu :dirtype='panfileStore.SelectDirType' />
|
||||
<MediaLibraryMenu v-if='menuShowMediaLibrary'
|
||||
:selectedItem='panfileStore.GetSelectedFirst()'
|
||||
:x='0'
|
||||
:y='0'
|
||||
@close='onHideRightMenuScroll' />
|
||||
</div>
|
||||
|
||||
<div id='PanRightShowUpload' :style="{ display: showDragUpload ? '' : 'none' }" @drop='onPanDrop'
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
import { menuCopySelectedFile, menuCreatShare, menuDownload, menuTrashSelectFile } from '../topbtns/topbtn'
|
||||
import { modalRename, modalShuXing } from '../../utils/modal'
|
||||
import PanDAL from '../pandal'
|
||||
import { usePanTreeStore } from '../../store'
|
||||
import { usePanTreeStore, useAppStore } from '../../store'
|
||||
import TreeStore from '../../store/treestore'
|
||||
import { MediaScanner } from '../../utils/mediaScanner'
|
||||
import message from '../../utils/message'
|
||||
import { computed } from 'vue'
|
||||
import { isAliyunUser as isAliyunAccountUser, isCloud123User } from '../../aliapi/utils'
|
||||
|
||||
const istree = true
|
||||
const pantreeStore = usePanTreeStore()
|
||||
const appStore = useAppStore()
|
||||
const mediaScanner = MediaScanner.getInstance()
|
||||
const isCloudUser = computed(() => isCloud123User(pantreeStore.user_id || '') || pantreeStore.drive_id === 'cloud123')
|
||||
const isAliyunAccount = computed(() => isAliyunAccountUser(pantreeStore.user_id || ''))
|
||||
|
||||
const props = defineProps({
|
||||
inputselectType: {
|
||||
@@ -16,13 +25,76 @@ const props = defineProps({
|
||||
|
||||
const handleRefresh = () => PanDAL.aReLoadOneDirToShow('', 'refresh', false)
|
||||
const handleExpandAll = (isExpand: boolean) => {
|
||||
const pantreeStore = usePanTreeStore()
|
||||
const drive_id = pantreeStore.drive_id
|
||||
const file_id = pantreeStore.selectDir.file_id
|
||||
const diridList = TreeStore.GetDirChildDirID(drive_id, file_id)
|
||||
const diridList = (() => {
|
||||
const result: string[] = []
|
||||
const visited = new Set<string>()
|
||||
const stack = [file_id]
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop() as string
|
||||
const children = TreeStore.GetDirChildDirID(drive_id, current)
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
if (visited.has(child)) continue
|
||||
visited.add(child)
|
||||
result.push(child)
|
||||
stack.push(child)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})()
|
||||
pantreeStore.mTreeExpandAll(diridList, isExpand)
|
||||
}
|
||||
|
||||
// 扫描媒体库方法
|
||||
const handleScanMediaLibrary = async () => {
|
||||
const selectDir = pantreeStore.selectDir
|
||||
if (!selectDir || !selectDir.file_id) {
|
||||
message.warning('请先选择要扫描的文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建符合 IAliGetFileModel 接口的文件夹对象
|
||||
const folder = {
|
||||
__v_skip: true,
|
||||
drive_id: pantreeStore.drive_id,
|
||||
file_id: selectDir.file_id,
|
||||
parent_file_id: selectDir.parent_file_id || '',
|
||||
name: selectDir.name,
|
||||
namesearch: selectDir.name.toLowerCase(),
|
||||
ext: '',
|
||||
mime_type: '',
|
||||
mime_extension: '',
|
||||
category: 'folder',
|
||||
icon: 'iconfolder',
|
||||
file_count: 0,
|
||||
size: 0,
|
||||
sizeStr: '',
|
||||
time: Date.now(),
|
||||
timeStr: new Date().toLocaleString(),
|
||||
starred: false,
|
||||
isDir: true,
|
||||
thumbnail: ''
|
||||
} as any // 临时使用 any 类型避免完整实现所有字段
|
||||
|
||||
await mediaScanner.scanFolder(folder, pantreeStore.drive_id)
|
||||
message.success(`开始扫描文件夹 "${selectDir.name}" 的媒体库`)
|
||||
|
||||
// 切换到媒体库标签页
|
||||
appStore.toggleTab('media')
|
||||
} catch (error) {
|
||||
console.error('媒体库扫描失败:', error)
|
||||
message.error('媒体库扫描失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中了有效的文件夹
|
||||
const isSelectedFolder = computed(() => {
|
||||
return pantreeStore.selectDir && pantreeStore.selectDir.file_id && pantreeStore.selectDir.file_id !== ''
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -58,11 +130,17 @@ const handleExpandAll = (isExpand: boolean) => {
|
||||
<template #icon><i class='iconfont iconfenxiang' /></template>
|
||||
<template #default>分享</template>
|
||||
</a-doption>
|
||||
<a-doption @click="() => menuCreatShare(istree, 'pan', 'backup_root')">
|
||||
<a-doption v-if="isAliyunAccount" @click="() => menuCreatShare(istree, 'pan', 'backup_root')">
|
||||
<template #icon><i class='iconfont iconrss' /></template>
|
||||
<template #default>快传</template>
|
||||
</a-doption>
|
||||
|
||||
<!-- 扫描媒体库 -->
|
||||
<a-doption @click="handleScanMediaLibrary">
|
||||
<template #icon><i class='iconfont iconshipin' /></template>
|
||||
<template #default>扫描媒体库</template>
|
||||
</a-doption>
|
||||
|
||||
<a-dsubmenu id="leftpansubmove" class="rightmenu" trigger="hover">
|
||||
<template #default>
|
||||
<div @click.stop="() => {}">
|
||||
|
||||
@@ -16,11 +16,48 @@ import {
|
||||
menuVideoXBT
|
||||
} from '../topbtns/topbtn'
|
||||
import { modalRename, modalShuXing } from '../../utils/modal'
|
||||
import { useSettingStore } from '../../store'
|
||||
import { useSettingStore, usePanFileStore, useAppStore, usePanTreeStore } from '../../store'
|
||||
import { computed } from 'vue'
|
||||
import { MediaScanner } from '../../utils/mediaScanner'
|
||||
import message from '../../utils/message'
|
||||
import { isAliyunUser as isAliyunAccountUser, isCloud123User } from '../../aliapi/utils'
|
||||
import { isWebDavDrive } from '../../utils/webdavClient'
|
||||
|
||||
let istree = false
|
||||
const settingStore = useSettingStore()
|
||||
const panFileStore = usePanFileStore()
|
||||
const appStore = useAppStore()
|
||||
const panTreeStore = usePanTreeStore()
|
||||
const mediaScanner = MediaScanner.getInstance()
|
||||
|
||||
// 扫描媒体库方法
|
||||
const handleScanMediaLibrary = async () => {
|
||||
const selectedFiles = panFileStore.GetSelected()
|
||||
if (selectedFiles.length === 0) {
|
||||
message.warning('请先选择要扫描的文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
// 只允许扫描文件夹
|
||||
const folders = selectedFiles.filter(file => file.isDir)
|
||||
if (folders.length === 0) {
|
||||
message.warning('请选择文件夹进行媒体库扫描')
|
||||
return
|
||||
}
|
||||
|
||||
// 扫描第一个选中的文件夹
|
||||
const folder = folders[0]
|
||||
try {
|
||||
await mediaScanner.scanFolder(folder, folder.drive_id)
|
||||
message.success(`开始扫描文件夹 "${folder.name}" 的媒体库`)
|
||||
|
||||
// 切换到媒体库标签页
|
||||
appStore.toggleTab('media')
|
||||
} catch (error) {
|
||||
console.error('媒体库扫描失败:', error)
|
||||
message.error('媒体库扫描失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
dirtype: {
|
||||
@@ -60,6 +97,15 @@ const isShowBtn = computed(() => {
|
||||
const isPic = computed(() => {
|
||||
return (props.dirtype === 'pic' && props.inputpicType == 'mypic')
|
||||
})
|
||||
const isCloudUser = computed(() => isCloud123User(panTreeStore.user_id || '') || panTreeStore.drive_id === 'cloud123')
|
||||
const isAliyunAccount = computed(() => isAliyunAccountUser(panTreeStore.user_id || ''))
|
||||
const isWebDav = computed(() => isWebDavDrive(panTreeStore.drive_id || panTreeStore.selectDir.drive_id))
|
||||
|
||||
// 检查是否选中了文件夹
|
||||
const isSelectedFolder = computed(() => {
|
||||
const selectedFiles = panFileStore.GetSelected()
|
||||
return selectedFiles.some(file => file.isDir)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -74,11 +120,18 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconfenxiang' /></template>
|
||||
<template #default>分享</template>
|
||||
</a-doption>
|
||||
<a-doption @click="() => menuCreatShare(istree, 'pan', 'backup_root')">
|
||||
<a-doption v-if="isAliyunAccount" @click="() => menuCreatShare(istree, 'pan', 'backup_root')">
|
||||
<template #icon><i class='iconfont iconrss' /></template>
|
||||
<template #default>快传</template>
|
||||
</a-doption>
|
||||
<a-dsubmenu v-if="dirtype !== 'pic'" id='rightpansubbiaoji' class='rightmenu' trigger='hover'>
|
||||
|
||||
<!-- 扫描媒体库 -->
|
||||
<a-doption v-show="isSelectedFolder && isShowBtn" @click="handleScanMediaLibrary">
|
||||
<template #icon><i class='iconfont iconshipin' /></template>
|
||||
<template #default>扫描媒体库</template>
|
||||
</a-doption>
|
||||
|
||||
<a-dsubmenu v-if="dirtype !== 'pic' && !isWebDav" id='rightpansubbiaoji' class='rightmenu' trigger='hover'>
|
||||
<template #default>
|
||||
<div @click.stop='() => {}'>
|
||||
<span class='arco-dropdown-option-icon'>
|
||||
@@ -131,16 +184,16 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconcopy' /></template>
|
||||
<template #default>复制到...</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isAliyunAccount && !isWebDav' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileEncTypeChange(istree)">
|
||||
<template #icon><i class='iconfont iconsafebox' /></template>
|
||||
<template #default>标记加密</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn && dirtype !== "mypic" || dirtype === "search"' class='danger' @click='() => menuTrashSelectFile(istree, false, isPic)'>
|
||||
<a-doption v-show='!isWebDav && (isShowBtn && dirtype !== "mypic" || dirtype === "search")' class='danger' @click='() => menuTrashSelectFile(istree, false, isPic)'>
|
||||
<template #icon><i class='iconfont icondelete' /></template>
|
||||
<template #default>放回收站</template>
|
||||
</a-doption>
|
||||
<a-dsubmenu v-if='dirtype !== "mypic"' class='rightmenu' trigger='hover'>
|
||||
<a-dsubmenu v-if='dirtype !== "mypic" && (isAliyunAccount || isWebDav)' class='rightmenu' trigger='hover'>
|
||||
<template #default>
|
||||
<span class='arco-dropdown-option-icon'><i class='iconfont iconrest'></i></span>彻底删除
|
||||
</template>
|
||||
@@ -184,12 +237,12 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconjietu' /></template>
|
||||
<template #default>雪碧图</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isAliyunAccount && !isWebDav' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileEncTypeChange(istree)">
|
||||
<template #icon><i class='iconfont iconsafebox' /></template>
|
||||
<template #default>标记加密</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isAliyunAccount && !isWebDav' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileClearHistory(istree)">
|
||||
<template #icon><i class='iconfont iconshipin' /></template>
|
||||
<template #default>清除历史</template>
|
||||
@@ -206,7 +259,7 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconlist' /></template>
|
||||
<template #default>复制文件名</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isselected && !isselectedmulti'
|
||||
<a-doption v-show='isselected && !isselectedmulti && !isCloudUser'
|
||||
@click='() => menuCopyFileTree()'>
|
||||
<template #icon><i class='iconfont iconnode-tree1' /></template>
|
||||
<template #default>复制目录树</template>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed } from 'vue'
|
||||
import { usePanTreeStore } from '../../store'
|
||||
import { isAliyunUser as isAliyunAccountUser, isCloud123User } from '../../aliapi/utils'
|
||||
import { isWebDavDrive } from '../../utils/webdavClient'
|
||||
|
||||
import {
|
||||
menuAddAlbumSelectFile,
|
||||
@@ -56,6 +59,10 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const istree = false
|
||||
const panTreeStore = usePanTreeStore()
|
||||
const isCloudUser = computed(() => isCloud123User(panTreeStore.user_id || '') || panTreeStore.drive_id === 'cloud123')
|
||||
const isAliyunAccount = computed(() => isAliyunAccountUser(panTreeStore.user_id || ''))
|
||||
const isWebDav = computed(() => isWebDavDrive(panTreeStore.drive_id || panTreeStore.selectDir.drive_id))
|
||||
const isShowBtn = computed(() => {
|
||||
return (props.dirtype === 'pic' && props.inputpicType != 'mypic')
|
||||
|| props.dirtype === 'mypic' || ['search', 'color', 'pan'].includes(props.dirtype)
|
||||
@@ -76,15 +83,15 @@ const isPic = computed(() => {
|
||||
@click="() => menuCreatShare(istree, 'pan', 'resource_root')">
|
||||
<i class='iconfont iconfenxiang' />分享
|
||||
</a-button>
|
||||
<a-button v-if='!isPic && dirtype != "video" && dirtype !== "search"' type='text' size='small' tabindex='-1' title='Ctrl+T'
|
||||
<a-button v-if='!isPic && dirtype != "video" && dirtype !== "search" && isAliyunAccount' type='text' size='small' tabindex='-1' title='Ctrl+T'
|
||||
@click="() => menuCreatShare(istree, 'pan', 'backup_root')">
|
||||
<i class='iconfont iconrss' />快传
|
||||
</a-button>
|
||||
<a-button v-if='!isPic && !isallfavored' type='text' size='small' tabindex='-1' title='Ctrl+G'
|
||||
<a-button v-if='!isPic && !isallfavored && isAliyunAccount' type='text' size='small' tabindex='-1' title='Ctrl+G'
|
||||
@click='() => menuFavSelectFile(istree, true)'>
|
||||
<i class='iconfont iconcrown' />收藏
|
||||
</a-button>
|
||||
<a-button v-if='!isPic && isallfavored' type='text' size='small' tabindex='-1' title='Ctrl+G'
|
||||
<a-button v-if='!isPic && isallfavored && isAliyunAccount' type='text' size='small' tabindex='-1' title='Ctrl+G'
|
||||
@click='() => menuFavSelectFile(istree, false)'>
|
||||
<i class='iconfont iconcrown2' />取消收藏
|
||||
</a-button>
|
||||
@@ -102,12 +109,12 @@ const isPic = computed(() => {
|
||||
<i class='iconfont icondelete' />删除<i class='iconfont icondown' />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption v-show='isShowBtn || dirtype === "search"' title='Ctrl+Delete' class='danger'
|
||||
<a-doption v-show='(isShowBtn || dirtype === "search") && !isWebDav' title='Ctrl+Delete' class='danger'
|
||||
@click='() => menuTrashSelectFile(istree, false, isPic)'>
|
||||
<template #icon><i class='iconfont icondelete' /></template>
|
||||
<template #default>放回收站</template>
|
||||
</a-doption>
|
||||
<a-dsubmenu class='rightmenu' trigger='hover'>
|
||||
<a-dsubmenu v-if='isAliyunAccount || isWebDav' class='rightmenu' trigger='hover'>
|
||||
<template #default>
|
||||
<span class='arco-dropdown-option-icon'><i class='iconfont iconrest'></i></span>彻底删除
|
||||
</template>
|
||||
@@ -149,17 +156,17 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconjietu' /></template>
|
||||
<template #default>雪碧图</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isAliyunAccount' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileEncTypeChange(istree)">
|
||||
<template #icon><i class='iconfont iconsafebox' /></template>
|
||||
<template #default>标记加密</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn && isallcolored' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isallcolored && isAliyunAccount' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileClearHistory(istree)">
|
||||
<template #icon><i class='iconfont iconshipin' /></template>
|
||||
<template #default>清除历史</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='isShowBtn && isallcolored' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
<a-doption v-show='isShowBtn && isallcolored && !isWebDav' type='text' size='small' tabindex='-1' title='Ctrl+M'
|
||||
@click="() => menuFileColorChange(istree, '')">
|
||||
<template #icon><i class='iconfont iconfangkuang' /></template>
|
||||
<template #default>清除标记</template>
|
||||
@@ -176,7 +183,7 @@ const isPic = computed(() => {
|
||||
<template #icon><i class='iconfont iconlist' /></template>
|
||||
<template #default>复制文件名</template>
|
||||
</a-doption>
|
||||
<a-doption v-show='!dirtype.includes("pic") && isselected && !isselectedmulti'
|
||||
<a-doption v-show='!dirtype.includes("pic") && isselected && !isselectedmulti && isAliyunAccount'
|
||||
@click='() => menuCopyFileTree()'>
|
||||
<template #icon><i class='iconfont iconnode-tree1' /></template>
|
||||
<template #default>复制目录树</template>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<a-dropdown
|
||||
id='medialibrarymenu'
|
||||
class='rightmenu'
|
||||
:popup-visible='true'
|
||||
style='z-index: 9999;'
|
||||
>
|
||||
<div style="position: fixed; pointer-events: none; width: 1px; height: 1px;" />
|
||||
<template #content>
|
||||
<a-doption @click="handleAddToLibrary" v-if="!isInLibrary && !isMediaLibraryFolder">
|
||||
<template #icon><i class='iconfont iconstar' /></template>
|
||||
<template #default>添加到媒体库</template>
|
||||
</a-doption>
|
||||
|
||||
<a-doption @click="handleScanFolder" v-if="selectedItem?.isdir && !isMediaLibraryFolder">
|
||||
<template #icon><i class='iconfont iconreload-1-icon' /></template>
|
||||
<template #default>扫描媒体文件</template>
|
||||
</a-doption>
|
||||
|
||||
<a-doption @click="handleViewInLibrary" v-if="isInLibrary && !isMediaLibraryFolder">
|
||||
<template #icon><i class='iconfont iconmovie' /></template>
|
||||
<template #default>在媒体库中查看</template>
|
||||
</a-doption>
|
||||
|
||||
<!-- 媒体库文件夹删除选项 -->
|
||||
<a-doption @click="handleRemoveFromLibrary" v-if="isInLibrary || isMediaLibraryFolder" class="danger">
|
||||
<template #icon><i class='iconfont icondelete' /></template>
|
||||
<template #default>从媒体库删除</template>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted } from 'vue'
|
||||
import { useAppStore } from '../../store'
|
||||
import { useMediaLibraryStore } from '../../store/medialibrary'
|
||||
import { MediaScanner } from '../../utils/mediaScanner'
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import message from '../../utils/message'
|
||||
import type { MediaLibraryFolder } from '../../types/media'
|
||||
|
||||
const props = defineProps<{
|
||||
selectedItem?: any
|
||||
mediaLibraryFolder?: MediaLibraryFolder | null
|
||||
x: number
|
||||
y: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const appStore = useAppStore()
|
||||
const mediaStore = useMediaLibraryStore()
|
||||
const mediaScanner = MediaScanner.getInstance()
|
||||
|
||||
// 计算属性
|
||||
const isInLibrary = computed(() => {
|
||||
if (!props.selectedItem) return false
|
||||
const folderId = `${props.selectedItem.drive_id}_${props.selectedItem.file_id}`
|
||||
return mediaStore.folders.some(f => f.id === folderId)
|
||||
})
|
||||
|
||||
// 是否是媒体库文件夹(从媒体库导航传来的)
|
||||
const isMediaLibraryFolder = computed(() => {
|
||||
return !!props.mediaLibraryFolder
|
||||
})
|
||||
|
||||
// 设置菜单位置
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const menuEl = document.getElementById('medialibrarymenu')
|
||||
if (menuEl) {
|
||||
const rect = menuEl.getBoundingClientRect()
|
||||
menuEl.style.left = props.x + 'px'
|
||||
menuEl.style.top = props.y + 'px'
|
||||
menuEl.style.position = 'fixed'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleAddToLibrary = async () => {
|
||||
if (!props.selectedItem || !props.selectedItem.isdir) {
|
||||
message.error('只能选择文件夹添加到媒体库')
|
||||
emit('close')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const folderName = await new Promise<string>((resolve) => {
|
||||
const name = prompt('请输入媒体库名称:', props.selectedItem.name)
|
||||
resolve(name || props.selectedItem.name)
|
||||
})
|
||||
|
||||
if (!folderName) {
|
||||
emit('close')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始扫描
|
||||
await mediaScanner.scanFolder(props.selectedItem, props.selectedItem.drive_id)
|
||||
|
||||
message.success(`已将 "${folderName}" 添加到媒体库`)
|
||||
|
||||
// 切换到媒体库标签页
|
||||
const appStore = useAppStore()
|
||||
appStore.toggleTab('media')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding to media library:', error)
|
||||
message.error('添加到媒体库失败')
|
||||
} finally {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleScanFolder = async () => {
|
||||
if (!props.selectedItem || !props.selectedItem.isdir) {
|
||||
message.error('只能选择文件夹进行扫描')
|
||||
emit('close')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await mediaScanner.scanFolder(props.selectedItem, props.selectedItem.drive_id)
|
||||
message.success('扫描完成')
|
||||
} catch (error) {
|
||||
console.error('Error scanning folder:', error)
|
||||
message.error('扫描失败')
|
||||
} finally {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewInLibrary = () => {
|
||||
// 切换到媒体库并显示该文件夹的内容
|
||||
console.log('View in library:', props.selectedItem.name)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleRemoveFromLibrary = () => {
|
||||
const targetFolder = props.mediaLibraryFolder || props.selectedItem
|
||||
if (!targetFolder) return
|
||||
|
||||
const folderName = props.mediaLibraryFolder ? targetFolder.name : props.selectedItem.name
|
||||
const folderId = props.mediaLibraryFolder ? targetFolder.id : `${props.selectedItem.drive_id}_${props.selectedItem.file_id}`
|
||||
|
||||
Modal.confirm({
|
||||
title: '删除文件夹',
|
||||
content: `确定要从媒体库删除"${folderName}"文件夹吗?这将删除该文件夹及其所有已刮削的媒体信息。`,
|
||||
okText: '删除',
|
||||
okButtonProps: { status: 'danger' },
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 删除文件夹和相关媒体
|
||||
mediaStore.removeFolder(folderId)
|
||||
message.success(`已从媒体库删除文件夹: ${folderName}`)
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 使用与FileRightMenu.vue相同的样式 */
|
||||
.rightmenu .arco-dropdown-option.danger {
|
||||
color: var(--color-danger-6);
|
||||
}
|
||||
|
||||
.rightmenu .arco-dropdown-option.danger:hover {
|
||||
background-color: var(--color-danger-light-1);
|
||||
color: var(--color-danger-6);
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,8 @@ import AliShare from '../../aliapi/share'
|
||||
import { usePanTreeStore } from '../../store'
|
||||
import message from '../../utils/message'
|
||||
import PanDAL from '../pandal'
|
||||
import { isAliyunUser, isCloud123User } from '../../aliapi/utils'
|
||||
import { isWebDavDrive } from '../../utils/webdavClient'
|
||||
|
||||
const props = defineProps({
|
||||
dirtype: {
|
||||
@@ -34,6 +36,7 @@ const props = defineProps({
|
||||
|
||||
const videoSelectType = ref('recent')
|
||||
const panTreeStore = usePanTreeStore()
|
||||
const isWebDav = computed(() => isWebDavDrive(panTreeStore.drive_id || panTreeStore.selectDir.drive_id))
|
||||
|
||||
const isShowBtn = computed(() => {
|
||||
return (props.dirtype === 'pic' && props.inputpicType != 'mypic')
|
||||
@@ -102,7 +105,7 @@ const handleClickBottleFish = async () => {
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-dgroup title="普通新建">
|
||||
<a-doption value='newfile' title='Ctrl+N' @click='() => modalCreatNewFile()'>
|
||||
<a-doption v-if='!isWebDav' value='newfile' title='Ctrl+N' @click='() => modalCreatNewFile()'>
|
||||
<template #icon><i class='iconfont iconwenjian' /></template>
|
||||
<template #default>新建文件</template>
|
||||
</a-doption>
|
||||
@@ -110,12 +113,12 @@ const handleClickBottleFish = async () => {
|
||||
<template #icon><i class='iconfont iconfile-folder' /></template>
|
||||
<template #default>新建文件夹</template>
|
||||
</a-doption>
|
||||
<a-doption value='newdatefolder' @click="() => modalCreatNewDir('datefolder')">
|
||||
<a-doption v-if='!isWebDav' value='newdatefolder' @click="() => modalCreatNewDir('datefolder')">
|
||||
<template #icon><i class='iconfont iconfolderadd' /></template>
|
||||
<template #default>日期+序号</template>
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup title="加密新建">
|
||||
<a-dgroup v-if='!isWebDav' title="加密新建">
|
||||
<a-doption value='newfile' @click='() => modalCreatNewFile("xbyEncrypt1")'>
|
||||
<template #icon><i class='iconfont iconwenjian' /></template>
|
||||
<template #default>新建文件(加密)</template>
|
||||
@@ -129,7 +132,7 @@ const handleClickBottleFish = async () => {
|
||||
<template #default>日期+序号(加密)</template>
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup title="私密新建">
|
||||
<a-dgroup v-if='!isWebDav' title="私密新建">
|
||||
<a-doption value='newfile' @click='() => modalCreatNewFile("xbyEncrypt2")'>
|
||||
<template #icon><i class='iconfont iconwenjian' /></template>
|
||||
<template #default>新建文件(私密)</template>
|
||||
@@ -150,7 +153,7 @@ const handleClickBottleFish = async () => {
|
||||
@click='modalCreatNewAlbum'>
|
||||
<i class='iconfont iconplus' />创建相册
|
||||
</a-button>
|
||||
<a-dropdown v-if='!dirtype.includes("pic")' trigger='hover' class='rightmenu' position='bl'>
|
||||
<a-dropdown v-if='!dirtype.includes("pic") && !isWebDav' trigger='hover' class='rightmenu' position='bl'>
|
||||
<a-button type='text' size='small' tabindex='-1'>
|
||||
<i class='iconfont iconupload' />上传<i class='iconfont icondown' />
|
||||
</a-button>
|
||||
@@ -166,7 +169,7 @@ const handleClickBottleFish = async () => {
|
||||
<template #default>上传文件夹</template>
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup title="加密上传">
|
||||
<a-dgroup v-if='!isWebDav' title="加密上传">
|
||||
<a-doption value='uploadfile' title='Ctrl+J'
|
||||
@click="() => handleUpload('file', 'xbyEncrypt1')">
|
||||
<template #icon><i class='iconfont iconwenjian' /></template>
|
||||
@@ -177,7 +180,7 @@ const handleClickBottleFish = async () => {
|
||||
<template #default>上传文件夹(加密)</template>
|
||||
</a-doption>
|
||||
</a-dgroup>
|
||||
<a-dgroup title="私密上传">
|
||||
<a-dgroup v-if='!isWebDav' title="私密上传">
|
||||
<a-doption value='uploadfile' title='Ctrl+M'
|
||||
@click="() => handleUpload('file', 'xbyEncrypt2')">
|
||||
<template #icon><i class='iconfont iconwenjian' /></template>
|
||||
@@ -190,7 +193,7 @@ const handleClickBottleFish = async () => {
|
||||
</a-dgroup>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-dropdown v-if="isShowBtn && dirtype.includes('pic')"
|
||||
<a-dropdown v-if="isShowBtn && dirtype.includes('pic') && isAliyunUser(panTreeStore.user_id || '')"
|
||||
trigger='hover' class='rightmenu' position='bl'>
|
||||
<a-button type='text' size='small' tabindex='-1'>
|
||||
<i class='iconfont iconupload' />上传照片/视频<i class='iconfont icondown' />
|
||||
@@ -219,7 +222,7 @@ const handleClickBottleFish = async () => {
|
||||
</a-dgroup>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button v-if="!dirtype.includes('pic')" type='text' size='small' tabindex='-1' title='Ctrl+L'
|
||||
<a-button v-if="!dirtype.includes('pic') && isAliyunUser(panTreeStore.user_id || '')" type='text' size='small' tabindex='-1' title='Ctrl+L'
|
||||
@click='modalDaoRuShareLink()'>
|
||||
<i class='iconfont iconlink2' />导入分享
|
||||
</a-button>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="showModal"
|
||||
:title="isEditMode ? '编辑收藏名称' : '添加到收藏夹'"
|
||||
:width="400"
|
||||
:footer="false"
|
||||
@cancel="handleCancel"
|
||||
modal-class="modal-class"
|
||||
@keydown.esc="handleCancel"
|
||||
@keydown.enter="handleConfirm">
|
||||
<div class="add-to-favorite-modal">
|
||||
<div class="form-item">
|
||||
<label class="form-label">收藏名称:</label>
|
||||
<a-input
|
||||
ref="inputRef"
|
||||
v-model="favoriteName"
|
||||
placeholder="请输入收藏名称"
|
||||
size="large"
|
||||
@keydown.enter="handleConfirm"
|
||||
@keydown.esc="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<a-button size="large" @click="handleCancel">取消</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="!favoriteName.trim()"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ isEditMode ? '保存' : '添加' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import { useModalStore } from '../../store'
|
||||
import { FavoritesManager } from '../../utils/favorites'
|
||||
import usePanTreeStore from '../../pan/pantreestore'
|
||||
import message from '../../utils/message'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const panTreeStore = usePanTreeStore()
|
||||
const show = computed(() => modalStore.modalName === 'addtofavorite')
|
||||
const modalData = computed(() => modalStore.modalData)
|
||||
|
||||
const showModal = ref(false)
|
||||
const favoriteName = ref('')
|
||||
const inputRef = ref()
|
||||
const isEditMode = ref(false)
|
||||
const editingId = ref('')
|
||||
|
||||
watch(show, async (newVal) => {
|
||||
if (newVal && modalData.value) {
|
||||
const { file_id, file_name, drive_id } = modalData.value
|
||||
|
||||
// 检查是否已经是收藏状态
|
||||
const allFavorites = FavoritesManager.getAllFavorites()
|
||||
const existingFavorite = allFavorites.find(
|
||||
item => item.file_id === file_id && item.drive_id === drive_id
|
||||
)
|
||||
|
||||
if (existingFavorite) {
|
||||
// 如果已经是收藏状态,进入编辑模式
|
||||
isEditMode.value = true
|
||||
editingId.value = existingFavorite.id
|
||||
favoriteName.value = existingFavorite.name
|
||||
} else {
|
||||
// 否则进入添加模式
|
||||
isEditMode.value = false
|
||||
editingId.value = ''
|
||||
favoriteName.value = file_name || ''
|
||||
}
|
||||
|
||||
showModal.value = true
|
||||
await nextTick()
|
||||
if (inputRef.value) {
|
||||
inputRef.value.focus()
|
||||
inputRef.value.select()
|
||||
}
|
||||
} else {
|
||||
showModal.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
modalStore.showModal('', {})
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!favoriteName.value.trim()) return
|
||||
|
||||
const { file_id, file_name, drive_id } = modalData.value
|
||||
|
||||
if (isEditMode.value && editingId.value) {
|
||||
// 编辑现有收藏
|
||||
if (FavoritesManager.updateFavoriteName(editingId.value, favoriteName.value.trim())) {
|
||||
message.success('更新收藏名称成功')
|
||||
// 更新收藏夹树
|
||||
// panTreeStore.mUpdateFavoriteTree()
|
||||
modalStore.showModal('', {})
|
||||
} else {
|
||||
message.error('更新收藏名称失败')
|
||||
}
|
||||
} else {
|
||||
// 添加新收藏
|
||||
const id = FavoritesManager.addFavorite({
|
||||
name: favoriteName.value.trim(),
|
||||
file_id,
|
||||
drive_id,
|
||||
original_name: file_name
|
||||
})
|
||||
|
||||
if (id) {
|
||||
message.success('添加收藏成功')
|
||||
// 更新收藏夹树
|
||||
// panTreeStore.mUpdateFavoriteTree()
|
||||
modalStore.showModal('', {})
|
||||
} else {
|
||||
message.error('添加收藏失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.add-to-favorite-modal {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.arco-input {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.arco-btn {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,20 @@
|
||||
import { IAliGetDirModel } from '../aliapi/alimodels'
|
||||
import AliFile from '../aliapi/file'
|
||||
import AliDirFileList from '../aliapi/dirfilelist'
|
||||
import AliDirFileList, { NewIAliFileResp } from '../aliapi/dirfilelist'
|
||||
import { ITokenInfo, useFootStore, usePanFileStore } from '../store'
|
||||
import TreeStore, { IDriverModel, TreeNodeData } from '../store/treestore'
|
||||
import DB from '../utils/db'
|
||||
import DebugLog from '../utils/debuglog'
|
||||
import message from '../utils/message'
|
||||
import usePanTreeStore from './pantreestore'
|
||||
import { GetDriveID, GetDriveType } from '../aliapi/utils'
|
||||
import { GetDriveID, GetDriveType, isBaiduUser, isCloud123User, isDrive115User } from '../aliapi/utils'
|
||||
import AliAlbum from '../aliapi/album'
|
||||
import { apiCloud123FileList, mapCloud123FileToAliModel } from '../cloud123/dirfilelist'
|
||||
import { apiDrive115Search, mapDrive115SearchToAliModel, mapDrive115TrashToAliModel } from '../cloud115/dirfilelist'
|
||||
import { apiDrive115TrashList } from '../cloud115/trash'
|
||||
import { apiDrive115FileList, mapDrive115FileToAliModel } from '../cloud115/dirfilelist'
|
||||
import { apiBaiduFileList, apiBaiduSearch, mapBaiduFileToAliModel } from '../cloudbaidu/dirfilelist'
|
||||
import { getWebDavConnection, getWebDavConnectionId, isWebDavDrive, listWebDavDirectory } from '../utils/webdavClient'
|
||||
|
||||
export interface PanSelectedData {
|
||||
isError: boolean
|
||||
@@ -25,8 +31,100 @@ export interface PanSelectedData {
|
||||
}
|
||||
|
||||
const RefreshLock = new Set<string>()
|
||||
const Drive115TrashPaging = new Map<string, { offset: number; total: number; limit: number; loading: boolean }>()
|
||||
|
||||
const resolveBaiduDirPath = (drive_id: string, dirID: string): string => {
|
||||
if (!dirID || dirID === 'baidu_root') return '/'
|
||||
if (dirID.startsWith('/')) return dirID
|
||||
const dir = TreeStore.GetDir(drive_id, dirID)
|
||||
if (dir?.path) return dir.path
|
||||
const selectDir: any = usePanTreeStore().selectDir
|
||||
if (selectDir?.file_id === dirID && selectDir?.path) return selectDir.path
|
||||
const desc = dir?.description || selectDir?.description || ''
|
||||
const match = /baidu_path:([^;]+)/.exec(desc)
|
||||
if (match && match[1]) return match[1]
|
||||
return '/'
|
||||
}
|
||||
|
||||
export default class PanDAL {
|
||||
static async aReLoadCloudDrive(token: ITokenInfo): Promise<void> {
|
||||
const { user_id } = token
|
||||
const drive_id = token.default_drive_id || token.resource_drive_id || 'cloud123'
|
||||
const pantreeStore = usePanTreeStore()
|
||||
pantreeStore.mSaveUser(user_id, drive_id, '', '', '')
|
||||
pantreeStore.drive_id = drive_id
|
||||
if (!user_id) return
|
||||
useFootStore().mSaveLoading('加载 123 网盘文件夹...')
|
||||
const list = await apiCloud123FileList(user_id, 0, 100)
|
||||
const driveType = GetDriveType(user_id, drive_id)
|
||||
const dirs = list
|
||||
.filter((item) => item.type === 1)
|
||||
.map((item) => ({
|
||||
file_id: String(item.fileId),
|
||||
drive_id: drive_id,
|
||||
parent_file_id: driveType.key,
|
||||
name: item.filename,
|
||||
description: '',
|
||||
time: new Date(item.updateAt || item.createAt || '').getTime() || 0,
|
||||
size: 0
|
||||
}))
|
||||
await TreeStore.ConvertToOneDriver(user_id, drive_id, dirs, false, true)
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
useFootStore().mSaveLoading('')
|
||||
}
|
||||
|
||||
static async aReLoadDrive115(token: ITokenInfo): Promise<void> {
|
||||
const { user_id } = token
|
||||
const drive_id = token.default_drive_id || 'drive115'
|
||||
const pantreeStore = usePanTreeStore()
|
||||
pantreeStore.mSaveUser(user_id, drive_id, '', '', '')
|
||||
pantreeStore.drive_id = drive_id
|
||||
if (!user_id) return
|
||||
useFootStore().mSaveLoading('加载 115 网盘文件夹...')
|
||||
const list = await apiDrive115FileList(user_id, 0, 200, 0, true)
|
||||
const driveType = GetDriveType(user_id, drive_id)
|
||||
const dirs = list
|
||||
.filter((item) => String(item.fc) === '0')
|
||||
.map((item) => ({
|
||||
file_id: String(item.fid),
|
||||
drive_id: drive_id,
|
||||
parent_file_id: driveType.key,
|
||||
name: item.fn,
|
||||
description: '',
|
||||
time: 0,
|
||||
size: 0
|
||||
}))
|
||||
await TreeStore.ConvertToOneDriver(user_id, drive_id, dirs, false, true)
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
useFootStore().mSaveLoading('')
|
||||
}
|
||||
|
||||
static async aReLoadBaiduDrive(token: ITokenInfo): Promise<void> {
|
||||
const { user_id } = token
|
||||
const drive_id = token.default_drive_id || 'baidu'
|
||||
const pantreeStore = usePanTreeStore()
|
||||
pantreeStore.mSaveUser(user_id, drive_id, '', '', '')
|
||||
pantreeStore.drive_id = drive_id
|
||||
if (!user_id) return
|
||||
useFootStore().mSaveLoading('加载 百度网盘文件夹...')
|
||||
const list = await apiBaiduFileList(user_id, '/', 'name', 0, 1000)
|
||||
const driveType = GetDriveType(user_id, drive_id)
|
||||
const dirs = list
|
||||
.filter((item) => item.isdir === 1)
|
||||
.map((item) => ({
|
||||
file_id: String(item.fs_id),
|
||||
drive_id: drive_id,
|
||||
parent_file_id: driveType.key,
|
||||
path: item.path,
|
||||
name: item.server_filename,
|
||||
description: item.fs_id ? `:${item.fs_id};baidu_path:${item.path}` : '',
|
||||
time: (item.server_mtime || item.server_ctime || 0) * 1000,
|
||||
size: 0
|
||||
}))
|
||||
await TreeStore.ConvertToOneDriver(user_id, drive_id, dirs, false, true)
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
useFootStore().mSaveLoading('')
|
||||
}
|
||||
|
||||
static async aReLoadBackupDrive(token: ITokenInfo): Promise<void> {
|
||||
const { user_id, default_drive_id, resource_drive_id, backup_drive_id, pic_drive_id } = token
|
||||
@@ -165,7 +263,11 @@ export default class PanDAL {
|
||||
}
|
||||
let dir = TreeStore.GetDir(drive_id, file_id)
|
||||
let dirPath = TreeStore.GetDirPath(drive_id, file_id)
|
||||
const isCloudUser = isCloud123User(user_id)
|
||||
if (!dir || (dirPath.length == 0 && !file_id.includes('root'))) {
|
||||
if (isCloudUser) {
|
||||
// 123 网盘不支持路径查询,依赖已加载的目录结构
|
||||
} else {
|
||||
let findPath = []
|
||||
if (!album_id) {
|
||||
findPath = await AliFile.ApiFileGetPath(panTreeStore.user_id, drive_id, file_id)
|
||||
@@ -176,6 +278,7 @@ export default class PanDAL {
|
||||
dirPath = findPath
|
||||
dir = { ...dirPath[dirPath.length - 1] }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dir || (dirPath.length == 0 && !file_id.includes('root'))) {
|
||||
message.error('出错,找不到指定的文件夹 ' + file_id)
|
||||
@@ -220,6 +323,206 @@ export default class PanDAL {
|
||||
return
|
||||
}
|
||||
|
||||
if (isWebDavDrive(drive_id)) {
|
||||
const connectionId = getWebDavConnectionId(drive_id)
|
||||
const connection = getWebDavConnection(connectionId)
|
||||
if (!connection) {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
message.warning('WebDAV 连接不存在,请重新连接')
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
const requestPath = dirID === '/' ? '/' : dirID
|
||||
listWebDavDirectory(connection, requestPath)
|
||||
.then(async (allItems) => {
|
||||
const items = hasFiles ? allItems : allItems.filter((item) => item.isDir)
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName || (dirID === '/' ? connection.name : dirID.split('/').pop() || connection.name))
|
||||
dir.items = items
|
||||
dir.itemsKey = new Set(items.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = items.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
if (!TreeStore.GetDriver(drive_id)) {
|
||||
await TreeStore.ConvertToOneDriver(user_id, drive_id, [], false, true)
|
||||
}
|
||||
await TreeStore.SaveOneDirFileList(dir, hasFiles)
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
message.warning('列出 WebDAV 文件夹失败 ' + (err?.message || ''))
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isCloud123User(user_id)) {
|
||||
const isTrash = dirID === 'trash'
|
||||
const isSearch = dirID.startsWith('search')
|
||||
const parentFileId = dirID === 'cloud_root' ? 0 : (isTrash ? 0 : dirID)
|
||||
let searchData = ''
|
||||
let searchMode = 0
|
||||
if (isSearch) {
|
||||
searchData = dirID.substring('search'.length).trim()
|
||||
searchMode = 0
|
||||
}
|
||||
apiCloud123FileList(user_id, parentFileId, 100, isTrash, searchData, searchMode)
|
||||
.then((list) => {
|
||||
const allItems = list.map((item) => {
|
||||
const mapped = mapCloud123FileToAliModel(item)
|
||||
mapped.drive_id = drive_id
|
||||
return mapped
|
||||
})
|
||||
const items = hasFiles ? allItems : allItems.filter((item) => item.isDir)
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName)
|
||||
dir.items = items
|
||||
dir.itemsKey = new Set(items.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = items.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
TreeStore.SaveOneDirFileList(dir, hasFiles).then(() => {
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isDrive115User(user_id)) {
|
||||
const isTrash = dirID === 'trash'
|
||||
const isSearch = dirID.startsWith('search')
|
||||
if (isTrash) {
|
||||
const limit = 200
|
||||
apiDrive115TrashList(user_id, limit, 0)
|
||||
.then(({ items, total }) => {
|
||||
const allItems = items.map((item) => mapDrive115TrashToAliModel(item, drive_id))
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName)
|
||||
dir.items = allItems
|
||||
dir.itemsKey = new Set(allItems.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = total || allItems.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
const pageKey = `${user_id}:${drive_id}:trash`
|
||||
Drive115TrashPaging.set(pageKey, { offset: allItems.length, total: dir.itemsTotal || 0, limit, loading: false })
|
||||
TreeStore.SaveOneDirFileList(dir, hasFiles).then(() => {
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (isSearch) {
|
||||
const searchValue = dirID.substring('search'.length).trim()
|
||||
if (!searchValue) {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
apiDrive115Search(user_id, searchValue, 200, 0)
|
||||
.then(({ items, total }) => {
|
||||
const allItems = items.map((item) => mapDrive115SearchToAliModel(item, drive_id))
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName)
|
||||
dir.items = allItems
|
||||
dir.itemsKey = new Set(allItems.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = total || allItems.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
TreeStore.SaveOneDirFileList(dir, hasFiles).then(() => {
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
const parentId = dirID === 'drive115_root' ? 0 : dirID
|
||||
apiDrive115FileList(user_id, parentId, 200, 0, true)
|
||||
.then((list) => {
|
||||
const allItems = list.map((item) => mapDrive115FileToAliModel(item, drive_id))
|
||||
const items = hasFiles ? allItems : allItems.filter((item) => item.isDir)
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName)
|
||||
dir.items = items
|
||||
dir.itemsKey = new Set(items.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = items.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
TreeStore.SaveOneDirFileList(dir, hasFiles).then(() => {
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isBaiduUser(user_id)) {
|
||||
const isSearch = dirID.startsWith('search')
|
||||
const dirPath = resolveBaiduDirPath(drive_id, dirID)
|
||||
const parentPath = isSearch ? '/' : dirPath
|
||||
const request = isSearch
|
||||
? apiBaiduSearch(user_id, dirID.substring('search'.length).trim(), '/', true)
|
||||
: apiBaiduFileList(user_id, dirPath, 'name', 0, 1000)
|
||||
request
|
||||
.then((list) => {
|
||||
const allItems = list.map((item) => mapBaiduFileToAliModel(item, drive_id, parentPath))
|
||||
const items = hasFiles ? allItems : allItems.filter((item) => item.isDir)
|
||||
const dir = NewIAliFileResp(user_id, drive_id, dirID, dirName)
|
||||
dir.items = items
|
||||
dir.itemsKey = new Set(items.map((item) => item.file_id))
|
||||
dir.next_marker = ''
|
||||
dir.itemsTotal = items.length
|
||||
const panfileStore = usePanFileStore()
|
||||
panfileStore.mSaveDirFileLoadingPart(0, dir, dir.itemsTotal || 0)
|
||||
TreeStore.SaveOneDirFileList(dir, hasFiles).then(() => {
|
||||
if (hasFiles) {
|
||||
panfileStore.mSaveDirFileLoadingFinish(drive_id, dirID, dir.items, dir.itemsTotal || 0)
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
if (hasFiles) usePanFileStore().mSaveDirFileLoadingFinish(drive_id, dirID, [])
|
||||
resolve(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const order = TreeStore.GetDirOrder(drive_id, dirID).replace('ext ', 'updated_at ')
|
||||
AliDirFileList.ApiDirFileList(user_id, drive_id, dirID, dirName, order, hasFiles ? '' : 'folder', albumID)
|
||||
.then((dir) => {
|
||||
@@ -262,31 +565,23 @@ export default class PanDAL {
|
||||
return
|
||||
}
|
||||
RefreshLock.add(dirID)
|
||||
const order = TreeStore.GetDirOrder(drive_id, dirID).replace('ext ', 'updated_at ')
|
||||
|
||||
AliDirFileList.ApiDirFileList(user_id, drive_id, dirID, '', order, 'folder', albumID)
|
||||
.then((dir) => {
|
||||
if (!dir.next_marker) {
|
||||
dir.dirID = dirID
|
||||
TreeStore.SaveOneDirFileList(dir, false).then(() => {
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
const pantreeStore = usePanTreeStore()
|
||||
if (pantreeStore.selectDir.drive_id == drive_id && (pantreeStore.selectDir.file_id == dirID)) {
|
||||
PanDAL.aReLoadOneDirToShow(drive_id, dirID, false, albumID).then(() => {
|
||||
RefreshLock.delete(dirID)
|
||||
resolve(true)
|
||||
})
|
||||
} else {
|
||||
RefreshLock.delete(dirID)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
} else if (dir.next_marker == 'cancel') {
|
||||
PanDAL.GetDirFileList(user_id, drive_id, dirID, '', albumID, false)
|
||||
.then((success) => {
|
||||
if (!success) {
|
||||
RefreshLock.delete(dirID)
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
PanDAL.RefreshPanTreeAllNode(drive_id)
|
||||
const pantreeStore = usePanTreeStore()
|
||||
if (pantreeStore.selectDir.drive_id == drive_id && pantreeStore.selectDir.file_id == dirID) {
|
||||
PanDAL.aReLoadOneDirToShow(drive_id, dirID, false, albumID).then(() => {
|
||||
RefreshLock.delete(dirID)
|
||||
resolve(true)
|
||||
})
|
||||
} else {
|
||||
RefreshLock.delete(dirID)
|
||||
resolve(false)
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { IAliGetDirModel } from '../aliapi/alimodels'
|
||||
import { h } from 'vue'
|
||||
import PanDAL from './pandal'
|
||||
import TreeStore, { TreeNodeData } from '../store/treestore'
|
||||
import { GetDriveID } from '../aliapi/utils'
|
||||
import { GetDriveID, isBaiduUser, isCloud123User, isDrive115User } from '../aliapi/utils'
|
||||
import message from '../utils/message'
|
||||
|
||||
export interface PanTreeState {
|
||||
user_id: string
|
||||
@@ -129,6 +130,20 @@ const usePanTreeStore = defineStore('pantree', {
|
||||
mTreeSelected(e: any, kuaijie: boolean = false) {
|
||||
let { key, drive_id = undefined } = e.node
|
||||
let is_refresh_drive_id = !['favorite', 'trash', 'recover'].includes(key) || !/color.*/g.test(key)
|
||||
const isCloudUser = isCloud123User(this.user_id || '')
|
||||
const isDrive115 = isDrive115User(this.user_id || '')
|
||||
const isBaidu = isBaiduUser(this.user_id || '')
|
||||
if (isCloudUser) {
|
||||
const unsupported = ['video', 'recover', 'pic_root', 'backup_root', 'resource_root', 'favorite']
|
||||
if (unsupported.includes(key)) {
|
||||
message.info('123云盘不支持此功能')
|
||||
return
|
||||
}
|
||||
}
|
||||
if ((isDrive115 || isBaidu) && key === 'resource_root') {
|
||||
message.info((isDrive115 ? '115网盘' : '百度网盘') + '不支持此功能')
|
||||
return
|
||||
}
|
||||
if (!kuaijie) {
|
||||
const getParentNode = (node: any): any => {
|
||||
return node.parent ? getParentNode(node.parent) : node
|
||||
@@ -191,11 +206,14 @@ const usePanTreeStore = defineStore('pantree', {
|
||||
mSaveTreeAllNode(drive_id: string, root: TreeNodeData, rootMap: Map<string, TreeNodeData>) {
|
||||
if (this.drive_id !== drive_id) return
|
||||
const list: TreeNodeData[] = []
|
||||
let hasRoot = false
|
||||
for (let i = 0, maxi = this.treeData.length; i < maxi; i++) {
|
||||
if (this.treeData[i].key == root.key) {
|
||||
list.push(root)
|
||||
hasRoot = true
|
||||
} else list.push(this.treeData[i])
|
||||
}
|
||||
if (!hasRoot) list.push(root)
|
||||
this.treeData = list
|
||||
treeDataMap = rootMap
|
||||
},
|
||||
|
||||