Update On Mon Mar 23 20:06:14 CET 2026

This commit is contained in:
github-action[bot]
2026-03-23 20:06:14 +01:00
parent e4ce3e16d9
commit b68247d4fe
639 changed files with 28453 additions and 6554 deletions
+1
View File
@@ -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
+35
View File
@@ -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)"
+5 -6
View File
@@ -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 }}
+15
View File
@@ -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/
+149
View File
@@ -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
+48 -15
View File
@@ -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 @@
# 功能 [![](https://img.shields.io/badge/-%E5%8A%9F%E8%83%BD-blue)](#功能-)
1.根据阿里云盘Open平台api开发的网盘客户端,支持win7-11macOSlinux <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>
# 界面 [![](https://img.shields.io/badge/-%E7%95%8C%E9%9D%A2-blue)](#界面-)
<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>
+25
View File
@@ -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>
+217
View File
@@ -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` 文件
- ✅ 配置统一,减少出错
- ✅ 适合桌面应用的使用场景
- ✅ 自动清理机制保证安全
+12 -2
View File
@@ -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",
+4 -4
View File
@@ -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) {
+1 -1
View File
@@ -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'),
+76 -8
View File
@@ -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
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

+1 -1
View File
@@ -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" />
+11 -6
View File
@@ -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"
]
}
}
+4193 -3015
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 -1
View File
@@ -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" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

+99
View File
@@ -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 };
+138
View File
@@ -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 };
+50
View File
@@ -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🚀 修复完成! 现在应该可以正常滚动浏览媒体内容了。')
+15
View File
@@ -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
+4
View File
@@ -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
+369 -172
View File
@@ -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'
+275 -1
View File
@@ -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) {
+259 -3
View File
@@ -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,
+76 -2
View File
@@ -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'
+86
View File
@@ -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
}
}
+5
View File
@@ -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)
+3 -2
View File
@@ -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
}
+156 -4
View File
@@ -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 = ''
+59
View File
@@ -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
+37
View File
@@ -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 []
}
}
+297
View File
@@ -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: ''
}
}
+45
View File
@@ -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 '获取下载地址失败'
}
}
+119
View File
@@ -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
}
}
+36
View File
@@ -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 []
}
}
+191
View File
@@ -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
}
+40
View File
@@ -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 }
}
}
+124
View File
@@ -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 []
}
}
+188
View File
@@ -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
}
+235
View File
@@ -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'
}
}
+147
View File
@@ -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
}
+155
View File
@@ -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: ''
}
}
+410
View File
@@ -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
}
+86
View File
@@ -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
}
+169
View File
@@ -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
}
+80
View File
@@ -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}`
}
+161
View File
@@ -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 '上传完成确认超时'
}
}
+62
View File
@@ -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 '获取转码列表失败'
}
}
+162
View File
@@ -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
}
}
+172
View File
@@ -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: ''
}
}
+66
View File
@@ -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)
}
+273
View File
@@ -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 }
}
+100
View File
@@ -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'
}
}
+219
View File
@@ -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>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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>
File diff suppressed because it is too large Load Diff
+314
View File
@@ -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
+10
View File
@@ -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>
+114 -4
View File
@@ -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)))
}
}
}
+16
View File
@@ -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'>
+17 -7
View File
@@ -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)
+4
View File
@@ -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 || ""' />
+19 -4
View File
@@ -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>
+161 -82
View File
@@ -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 () => {
+5 -1
View File
@@ -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 = []
+45 -18
View File
@@ -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'
+32 -9
View File
@@ -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'
+82 -4
View File
@@ -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="() => {}">
+62 -9
View File
@@ -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>
+16 -9
View File
@@ -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 -9
View File
@@ -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>
+318 -23
View File
@@ -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) => {
+19 -1
View File
@@ -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
},

Some files were not shown because too many files have changed in this diff Show More