Compare commits

..

28 Commits

Author SHA1 Message Date
q
0579588814 优化构建流v0.1.9b6b 2025-07-10 19:17:22 +08:00
q
df2bfb6ac7 优化构建流 2025-07-10 19:12:15 +08:00
q
517b6f8910 del 2025-07-10 19:07:02 +08:00
q
94a46d2833 目录解析支持优化 v0.1.9b6 2025-07-10 18:59:59 +08:00
q
1631a0faa1 目录解析支持优化 v0.1.9b5 2025-07-10 18:58:12 +08:00
qaiu
06d5943cb6 Merge remote-tracking branch 'origin/main' 2025-07-09 07:58:10 +08:00
qaiu
3095e13676 ye目录解析 2025-07-09 07:57:47 +08:00
qaiu
482cbce7e8 Update maven.yml 2025-07-09 07:09:58 +08:00
qaiu
ef2fc3ab98 lz目录解析预览 2025-07-09 07:06:12 +08:00
q
5b57b05eae 目录解析支持优化 v0.1.9b2 2025-07-08 18:58:38 +08:00
q
093579c6f5 Merge remote-tracking branch 'origin/main' 2025-07-08 18:57:17 +08:00
q
c2d4990d7f 目录解析支持优化 v0.1.9b2 2025-07-08 18:55:19 +08:00
qaiu
40e8380738 更新 README.md 2025-07-08 04:03:42 +08:00
qaiu
b716e1e861 更新 README.md 2025-07-08 04:02:50 +08:00
qaiu
8432d4952c 更新 README.md 2025-07-08 03:51:46 +08:00
qaiu
dd8f085f63 更新 README.md 2025-07-08 03:51:00 +08:00
qaiu
161ff8d8a3 更新 README.md 2025-07-08 03:48:19 +08:00
qaiu
1390cd0104 更新 README.md 2025-07-08 03:47:09 +08:00
qaiu
7a02b1e97f 更新 README.md 2025-07-08 03:46:19 +08:00
qaiu
036f107c90 更新 README.md
docker镜像更新
2025-07-08 03:20:18 +08:00
qaiu
5652383450 Update README.md 2025-07-08 02:23:01 +08:00
qaiu
9a047a5da0 更新 README.md 2025-07-04 19:38:18 +08:00
qaiu
8975743a37 更新 README.md 2025-07-04 19:34:53 +08:00
q
0e30eafe49 目录解析支持 2025-07-04 19:20:06 +08:00
q
7facb62f21 Merge remote-tracking branch 'origin/main' 2025-07-04 19:17:35 +08:00
q
30d43cb961 目录解析支持 2025-07-04 19:16:36 +08:00
q
c505b17e35 目录解析支持 2025-07-04 19:11:39 +08:00
qaiu
080c4c753d Create update-release-badge.yml 2025-07-04 09:34:22 +08:00
45 changed files with 3911 additions and 597 deletions

View File

@@ -16,8 +16,9 @@ permissions:
on:
push:
tags:
- '*'
branches: [ "main" ]
- '*' # 只有推送tag时才会触发构建
branches-ignore:
- '*' # 排除所有分支的提交
paths-ignore:
- 'bin/**'
- '.github/**'
@@ -27,7 +28,8 @@ on:
- '*.txt'
- '*.md'
pull_request:
branches: [ "main" ]
branches:
- "main"
jobs:
build:

View File

@@ -3,7 +3,7 @@
</p>
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml/badge.svg?style=flat"></a>
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b2&style=flat"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-blue?style=flat"></a>
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
@@ -76,23 +76,28 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
API规则:
> 建议使用UrlEncode编码分享链接
```
1. 解析并自动302跳转
http://your_host/parser?url=分享链接&pwd=xxx
或者 http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
http://your_host/parser?url=分享链接&pwd=xxx
http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
http://your_host/d/网盘标识/分享key@分享密码
2. 获取解析后的直链--JSON格式
http://your_host/json/parser?url=分享链接&pwd=xxx
http://your_host/json/parser?url=分享链接&pwd=xxx
http://your_host/json/网盘标识/分享key@分享密码
3. 文件夹解析v0.1.8fixed3新增
http://your_host/json/getFileList?url=分享链接&pwd=xxx
```
### json接口说明
1. 文件解析:/json/parser?url=分享链接&pwd=xxx
json返回数据格式示例:
`shareKey`: 全局分享key
`directLink`: 下载链接
`cacheHit`: 是否为缓存链接
`expires`: 缓存到期时间
```json
{
"code": 200,
@@ -139,6 +144,8 @@ json返回数据格式示例:
}
```
3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
/v2/getFileList?url=分享链接&pwd=分享密码
```json
{
"code": 200,
@@ -151,15 +158,15 @@ json返回数据格式示例:
"fileIcon": null,
"size": 999,
"sizeStr": "999 M",
"fileType": "apk",
"fileType": "file/folder",
"filePath": null,
"createTime": "17 小时前",
"updateTime": null,
"createBy": null,
"description": null,
"downloadCount": null,
"downloadCount": 下载次数,
"panType": "lz",
"parserUrl": "下载链接",
"parserUrl": "下载链接/文件夹链接",
"extParameters": null
}
]
@@ -260,15 +267,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.io/qaiu/netdisk-fast-download:main
docker pull ghcr.io/qaiu/netdisk-fast-download:lastest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:main
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:lastest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:main
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:lastest
# 反代6401端口
@@ -283,15 +290,15 @@ mkdir -p netdisk-fast-download
cd netdisk-fast-download
# 拉取镜像
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
# 复制配置文件或下载仓库web-service\src\main\resources
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
docker cp netdisk-fast-download:/app/resources ./resources
docker rm netdisk-fast-download
# 启动容器
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:main
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:lastest
# 反代6401端口

86
bin/nfd-install.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
set -e
# ----------- 配置区域 ------------
# JRE 下载目录
JRE_DIR="/opt/custom-jre17"
# 使用阿里云镜像下载 JREOpenJDK 17
JRE_TARBALL_URL="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/linux/OpenJDK17U-jre_x64_linux_hotspot_17.0.15_6.tar.gz"
# ZIP 文件下载相关
ZIP_URL="http://www.722shop.top:6401/parser?url="
ZIP_DEST_DIR="/opt/target-zip"
ZIP_FILE_NAME="nfd.zip"
# --------------------------------
# 创建目录
mkdir -p "$JRE_DIR"
mkdir -p "$ZIP_DEST_DIR"
# -------- 检查 unzip 是否存在 --------
if ! command -v unzip >/dev/null 2>&1; then
echo "unzip 未安装,正在安装..."
if command -v apt-get >/dev/null 2>&1; then
apt-get update && apt-get install -y unzip
elif command -v yum >/dev/null 2>&1; then
yum install -y unzip
elif command -v dnf >/dev/null 2>&1; then
dnf install -y unzip
else
echo "不支持的包管理器,无法自动安装 unzip请手动安装后重试。"
exit 1
fi
else
echo "unzip 已安装"
fi
# -------- 下载并解压 JRE --------
echo "下载 JRE 17 到 $JRE_DIR..."
curl -L "$JRE_TARBALL_URL" -o "$JRE_DIR/jre17.tar.gz"
echo "解压 JRE..."
tar -xzf "$JRE_DIR/jre17.tar.gz" -C "$JRE_DIR" --strip-components=1
rm "$JRE_DIR/jre17.tar.gz"
echo "JRE 解压完成"
# -------- 下载 ZIP 文件 --------
ZIP_PATH="$ZIP_DEST_DIR/$ZIP_FILE_NAME"
echo "下载 ZIP 文件到 $ZIP_PATH..."
curl -L "$ZIP_URL" -o "$ZIP_PATH"
# -------- 解压 ZIP 文件 --------
echo "解压 ZIP 文件到 $ZIP_DEST_DIR..."
unzip -o "$ZIP_PATH" -d "$ZIP_DEST_DIR"
echo "解压完成"
# -------- 启动 JAR 程序 --------
echo "进入 JAR 目录并后台运行程序..."
JAR_DIR="/opt/target-zip/netdisk-fast-download"
JAR_FILE="netdisk-fast-download.jar"
JAVA_BIN="$JRE_DIR/bin/java"
LOG_FILE="$JAR_DIR/app.log"
if [ ! -d "$JAR_DIR" ]; then
echo "[错误] 找不到 JAR 目录: $JAR_DIR"
exit 1
fi
cd "$JAR_DIR"
if [ ! -f "$JAR_FILE" ]; then
echo "[错误] 找不到 JAR 文件: $JAR_FILE"
exit 1
fi
if [ ! -x "$JAVA_BIN" ]; then
echo "[错误] 找不到可执行的 java: $JAVA_BIN"
exit 1
fi
# 后台运行,日志记录
nohup "$JAVA_BIN" -jar "$JAR_FILE" > "$LOG_FILE" 2>&1 &
echo "程序已在后台启动 ✅"
echo "日志路径: $LOG_FILE"

View File

@@ -16,17 +16,24 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
default Handler<RoutingContext> doHandle() {
return ctx -> {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
// 加同步锁
synchronized (BeforeInterceptor.class) {
ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx);
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
sendError(ctx, 403);
}
}
};
}
default void doNext(RoutingContext context) {
context.put(IS_NEXT, true);
context.next();
// 设置上下文状态为可以继续执行
// 添加同步锁保障多线程下执行时序
synchronized (BeforeInterceptor.class) {
context.put(IS_NEXT, true);
context.next();
}
}
void handle(RoutingContext context);

View File

@@ -69,6 +69,9 @@ public class FileInfo {
*/
private String parserUrl;
//预览地址
private String previewUrl;
/**
* 扩展参数
*/
@@ -199,6 +202,13 @@ public class FileInfo {
this.parserUrl = parserUrl;
return this;
}
public String getPreviewUrl() {
return previewUrl;
}
public FileInfo setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
return this;
}
public Map<String, Object> getExtParameters() {
return extParameters;

View File

@@ -15,6 +15,7 @@ public class ShareLinkInfo {
/**
* 其他参数预定义
* dirId: 目录ID 传入
* auths: 认证相关 传入
* UA: 浏览器请求头 传入
* fileInfo: 解析成功的文件信息对象 传出

View File

@@ -19,7 +19,7 @@ public interface IPanTool {
*/
default Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
promise.complete();
promise.fail("Not implemented yet");
return promise.future();
}
@@ -29,7 +29,7 @@ public interface IPanTool {
*/
default Future<String> parseById() {
Promise<String> promise = Promise.promise();
promise.complete();
promise.complete("Not implemented yet");
return promise.future();
}
}

View File

@@ -116,12 +116,21 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(Throwable t, String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
log.error("解析异常: " + s, t.fillInStackTrace());
promise.fail(baseMsg() + ": 解析异常: " + s + " -> " + t);
} catch (Exception e) {
log.error("ErrorMsg format fail. The parameter has been discarded", e);
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
promise.fail(baseMsg() + ": 解析异常: " + errorMsg + " -> " + t);
}
}
@@ -134,9 +143,18 @@ public abstract class PanBase implements IPanTool {
*/
protected void fail(String errorMsg, Object... args) {
try {
// 判断是否已经完成
if (promise.future().isComplete()) {
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
return;
}
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
promise.fail(baseMsg() + " - 解析异常: " + s);
} catch (Exception e) {
if (promise.future().isComplete()) {
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
return;
}
log.error("ErrorMsg format fail. The parameter has been discarded", e);
promise.fail(baseMsg() + " - 解析异常: " + errorMsg);
}

View File

@@ -14,6 +14,7 @@ import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Base64;
@@ -89,6 +90,9 @@ public class FjTool extends PanBase {
final String shareId = shareLinkInfo.getShareKey();
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
@@ -96,7 +100,7 @@ public class FjTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(FIRST_REQUEST_URL))
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
@@ -107,13 +111,21 @@ public class FjTool extends PanBase {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
@@ -158,104 +170,112 @@ public class FjTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
/*
{
"iconId" : 13,
"fileName" : "酷我音乐车机版 6.4.2.20.apk",
"fileSaves" : 52,
"fileStars" : 5.0,
"type" : 1,
"userId" : 1392902,
"fileComments" : 0,
"fileSize" : 68854,
"fileIcon" : "https://d.feijix.com/storage/files/icon/2024/06/08/7/8146637/6534494874910391.gz?t=67a5ea7c&rlimit=20&us=nMfuftjBN5&sign=f72be03007a301217f90dcc20333bd9a",
"updTime" : "2024-06-10 17:26:53",
"sortId" : 1487918143,
"name" : "酷我音乐车机版 6.4.2.20.apk",
"fileDownloads" : 109,
"fileUrl" : null,
"fileLikes" : 0,
"fileType" : 1,
"fileId" : 1487918143
}
*/
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
parserDir(id, shareId, promise);
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
// 映射已知字段fileInfo
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileJson.getString("fileId"))
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
});
}
@Override
public Future<String> parseById() {
// 第二次请求
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
// clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
// .putHeaders(header)
// .setTemplateParam("fidEncode", fidEncode)
// .setTemplateParam("uuid", uuid)
// .setTemplateParam("ts", tsEncode2)
// .setTemplateParam("auth", auth)
// .setTemplateParam("dataKey", shareId)
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))

View File

@@ -12,6 +12,7 @@ import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
@@ -77,17 +78,15 @@ public class IzTool extends PanBase {
// 第一次请求 获取文件信息
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
client.postAbs(UriTemplate.of(
shareLinkInfo.getSharePassword() == null ?
FIRST_REQUEST_URL : (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword()))
)
client.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
@@ -98,14 +97,21 @@ public class IzTool extends PanBase {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (resJson.getJsonArray("list").size() == 0) {
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
@@ -143,65 +149,102 @@ public class IzTool extends PanBase {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
parserDir(id, shareId, promise);
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID
client.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
JsonObject jsonObject = asJson(res);
System.out.println(jsonObject.encodePrettily());
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType(fileJson.getString("fileType"))
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
});
promise.complete(result);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
});
return promise.future();
promise.complete(result);
});
}
@Override

View File

@@ -222,7 +222,10 @@ public class LzTool extends PanBase {
.setSizeStr(fileJson.getString("size"))
.setSize(sizeNum)
.setPanType(panType)
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id);
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
shareLinkInfo.getType(), id));
;
log.debug("文件信息: {}", fileInfo);
list.add(fileInfo);
});

View File

@@ -1,11 +1,14 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import cn.qaiu.util.JsExecUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer;
@@ -15,7 +18,9 @@ import org.apache.commons.lang3.StringUtils;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -32,7 +37,7 @@ public class YeTool extends PanBase {
private static final String GET_FILE_INFO_URL = "https://www.123pan.com/a/api/share/get?limit=100&next=1&orderBy" +
"=file_name&orderDirection=asc" +
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1&event=homeListFile&operateType=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
@@ -97,6 +102,7 @@ public class YeTool extends PanBase {
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", "0")
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
.putHeader("Platform", "web")
.putHeader("App-Version", "3")
@@ -227,4 +233,121 @@ public class YeTool extends PanBase {
}
}).onFailure(this.handleFail(DOWNLOAD_API_URL));
}
// dir parser
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareKey = shareLinkInfo.getShareKey(); // 分享链接的唯一标识
String pwd = shareLinkInfo.getSharePassword(); // 分享密码
String parentFileId = "0"; // 根目录的文件ID
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (StringUtils.isNotBlank(dirId)) {
parentFileId = dirId;
}
// 构造文件列表接口的URL
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", pwd)
.setTemplateParam("ParentFileId", parentFileId)
.putHeaders(header)
.send().onSuccess(res -> {
JsonObject response = asJson(res);
if (response.getInteger("code") != 0) {
promise.fail("API错误: " + response.getString("message"));
return;
}
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
List<FileInfo> result = new ArrayList<>();
// 遍历返回的文件和目录信息
for (int i = 0; i < infoList.size(); i++) {
JsonObject item = infoList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
// "FileId": 16603582,
// "FileName": "pdf",
// "Type": 1,
// "Size": 0,
// "ContentType": "0",
// "S3KeyFlag": "",
// "CreateAt": "2025-07-09T06:56:20+08:00",
// "UpdateAt": "2025-07-09T06:56:20+08:00",
// "Etag": "",
// "DownloadUrl": "",
// "Status": 0,
// "ParentFileId": 16603579,
// "Category": 0,
// "PunishFlag": 0,
// "StorageNode": "m0",
// "PreviewType": 0
// =>
// {
// "ShareKey":"iaKtVv-FTaCd",
// "FileID":16604189,
// "S3keyFlag":"1815268665-0",
// "Size":425929,
// "Etag":"70049de67075ab2b269c62d690424601",
// "OrderId":""}
JsonObject postData = JsonObject.of()
.put("ShareKey", shareKey)
.put("FileID", item.getInteger("FileId"))
.put("S3keyFlag", item.getString("S3KeyFlag"))
.put("Size", item.getLong("Size"))
.put("Etag", item.getString("Etag"));
byte[] encode = Base64.getEncoder().encode(postData.encode().getBytes());
String param = new String(encode);
if (item.getInteger("Type") == 0) { // 文件
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getString("FileId"))
.setFileType("file")
.setSize(item.getLong("Size"))
.setCreateTime(item.getString("CreateAt"))
.setUpdateTime(item.getString("UpdateAt"))
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")))
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
} else if (item.getInteger("Type") == 1) { // 目录
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getString("FileId"))
.setCreateTime(item.getString("CreateAt"))
.setUpdateTime(item.getString("UpdateAt"))
.setSize(0L)
.setFileType("folder")
.setParserUrl(
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
getDomainName(),
shareLinkInfo.getShareUrl(),
item.getString("FileId"),
pwd)
);
result.add(fileInfo);
}
}
promise.complete(result);
}).onFailure(promise::fail);
return promise.future();
}
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
// 调用下载接口获取直链
down(client, paramJson, DOWNLOAD_API_URL);
return promise.future();
}
}

View File

@@ -10,7 +10,7 @@
## 关于如何将前端项目和java一块打包:
1. 先打包前端模块
2. ~~打包后请将当前目录下的nfd-front目录放置在项目下webroot目录, 然后使用maven打包java模块即可~~ `npm run build` 会直接打包到后端代理目录下, 无需复制
2. 运行`npm run build`
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
## nginx配置

View File

@@ -1,5 +1,8 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@vue/babel-plugin-transform-vue-jsx'
]
}

View File

@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
@@ -15,18 +16,22 @@
"core-js": "^3.8.3",
"element-plus": "^2.8.7",
"qrcode": "^1.5.4",
"splitpanes": "^4.0.4",
"vue": "^3.5.12",
"vue-clipboard3": "^2.0.0",
"vue-router": "^4.5.1",
"vue3-json-viewer": "2.2.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.26.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.14.0",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.30.0",
"filemanager-webpack-plugin": "8.0.0"
},
@@ -48,5 +53,12 @@
"> 1%",
"last 2 versions",
"not dead"
]
],
"engines": {
"node": ">=16.0.0 <=22.0.0",
"npm": ">=8.0.0"
},
"overrides": {
"eslint": "^9.0.0"
}
}

View File

@@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2024 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

9
web-front/public/css/all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -9,6 +9,8 @@
content="Netdisk fast download,网盘直链解析工具">
<meta name="description"
content="Netdisk fast download 网盘直链解析工具">
<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.page-loading-wrap {
padding: 120px;

693
web-front/public/list.html Normal file
View File

@@ -0,0 +1,693 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网盘目录管理系统</title>
<!-- 本地引用Font Awesome -->
<link rel="stylesheet" href="./css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
}
h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 10px;
position: relative;
display: inline-block;
}
h1:after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: linear-gradient(90deg, #3498db, #2c3e50);
border-radius: 2px;
}
.subtitle {
color: #7f8c8d;
font-size: 1.1rem;
margin-top: 5px;
}
.dashboard {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.breadcrumb {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #eaeaea;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #7f8c8d;
cursor: pointer;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: #3498db;
}
.breadcrumb-item i {
margin: 0 8px;
font-size: 0.8rem;
color: #bdc3c7;
}
.content {
padding: 24px;
min-height: 500px;
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
}
.item {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
padding: 20px 10px;
border: 2px solid transparent;
}
.item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
.item-icon {
font-size: 3.5rem;
margin-bottom: 15px;
transition: transform 0.3s;
}
.item:hover .item-icon {
transform: scale(1.1);
}
.folder .item-icon {
color: #3498db;
}
.image .item-icon {
color: #e74c3c;
}
.document .item-icon {
color: #f39c12;
}
.archive .item-icon {
color: #9b59b6;
}
.audio .item-icon {
color: #1abc9c;
}
.video .item-icon {
color: #d35400;
}
.code .item-icon {
color: #27ae60;
}
.item-name {
font-weight: 500;
font-size: 0.95rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.item-meta {
font-size: 0.8rem;
color: #95a5a6;
margin-top: 8px;
}
.empty-state {
text-align: center;
padding: 50px 20px;
color: #7f8c8d;
grid-column: 1 / -1;
}
.empty-state i {
font-size: 5rem;
margin-bottom: 20px;
color: #bdc3c7;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #2c3e50;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
grid-column: 1 / -1;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(52, 152, 219, 0.2);
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 15px 24px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background: #2980b9;
}
.btn i {
font-size: 0.9rem;
}
.stats {
display: flex;
align-items: center;
gap: 15px;
color: #7f8c8d;
font-size: 0.9rem;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
@media (max-width: 768px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 15px;
}
.item {
padding: 15px 8px;
}
.item-icon {
font-size: 3rem;
}
}
@media (max-width: 480px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
h1 {
font-size: 2rem;
}
.action-bar {
flex-direction: column;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-cloud"></i> 网盘目录管理系统</h1>
<p class="subtitle">管理您的文件与文件夹,操作简单直观</p>
</header>
<div class="dashboard">
<div class="breadcrumb" id="breadcrumb">
<!-- 面包屑导航会通过JS动态生成 -->
</div>
<div class="content">
<div class="grid-view" id="file-grid">
<!-- 文件列表会通过JS动态生成 -->
</div>
</div>
<div class="action-bar">
<button class="btn" id="back-btn">
<i class="fas fa-arrow-left"></i> 返回上一级
</button>
<div class="stats">
<div class="stat-item">
<i class="fas fa-folder"></i> <span id="folder-count">0</span> 个文件夹
</div>
<div class="stat-item">
<i class="fas fa-file"></i> <span id="file-count">0</span> 个文件
</div>
</div>
</div>
</div>
</div>
<script>
// 文件类型映射
const fileTypeIcons = {
// 图片
'jpg': { icon: 'fa-file-image', type: 'image' },
'jpeg': { icon: 'fa-file-image', type: 'image' },
'png': { icon: 'fa-file-image', type: 'image' },
'gif': { icon: 'fa-file-image', type: 'image' },
'bmp': { icon: 'fa-file-image', type: 'image' },
'svg': { icon: 'fa-file-image', type: 'image' },
'webp': { icon: 'fa-file-image', type: 'image' },
// 文档
'pdf': { icon: 'fa-file-pdf', type: 'document' },
'doc': { icon: 'fa-file-word', type: 'document' },
'docx': { icon: 'fa-file-word', type: 'document' },
'xls': { icon: 'fa-file-excel', type: 'document' },
'xlsx': { icon: 'fa-file-excel', type: 'document' },
'ppt': { icon: 'fa-file-powerpoint', type: 'document' },
'pptx': { icon: 'fa-file-powerpoint', type: 'document' },
'txt': { icon: 'fa-file-alt', type: 'document' },
'rtf': { icon: 'fa-file-alt', type: 'document' },
// 压缩文件
'zip': { icon: 'fa-file-archive', type: 'archive' },
'rar': { icon: 'fa-file-archive', type: 'archive' },
'7z': { icon: 'fa-file-archive', type: 'archive' },
'tar': { icon: 'fa-file-archive', type: 'archive' },
'gz': { icon: 'fa-file-archive', type: 'archive' },
// 音频
'mp3': { icon: 'fa-file-audio', type: 'audio' },
'wav': { icon: 'fa-file-audio', type: 'audio' },
'ogg': { icon: 'fa-file-audio', type: 'audio' },
'flac': { icon: 'fa-file-audio', type: 'audio' },
// 视频
'mp4': { icon: 'fa-file-video', type: 'video' },
'avi': { icon: 'fa-file-video', type: 'video' },
'mov': { icon: 'fa-file-video', type: 'video' },
'wmv': { icon: 'fa-file-video', type: 'video' },
'mkv': { icon: 'fa-file-video', type: 'video' },
'flv': { icon: 'fa-file-video', type: 'video' },
// 代码
'html': { icon: 'fa-file-code', type: 'code' },
'htm': { icon: 'fa-file-code', type: 'code' },
'css': { icon: 'fa-file-code', type: 'code' },
'js': { icon: 'fa-file-code', type: 'code' },
'json': { icon: 'fa-file-code', type: 'code' },
'php': { icon: 'fa-file-code', type: 'code' },
'py': { icon: 'fa-file-code', type: 'code' },
'java': { icon: 'fa-file-code', type: 'code' },
'c': { icon: 'fa-file-code', type: 'code' },
'cpp': { icon: 'fa-file-code', type: 'code' },
'h': { icon: 'fa-file-code', type: 'code' },
'sh': { icon: 'fa-file-code', type: 'code' },
'bat': { icon: 'fa-file-code', type: 'code' },
'md': { icon: 'fa-file-code', type: 'code' },
// 默认
'default': { icon: 'fa-file', type: 'document' }
};
const obj = new URL(window.location.href);
// 获取 URL 参数
const params = obj.searchParams;
const shareUrl = params.get('url');
const pwd = params.get('pwd');
// 动态拼接并编码参数
const apiUrl = `${window.location.origin}/v2/getFileList?url=${encodeURIComponent(shareUrl)}&pwd=${encodeURIComponent(pwd)}`;
// 当前目录状态
let currentDir = {
// url: 'http://192.168.101.227:6401/v2/getFileList?url=https://share.feijipan.com/s/3pMsofZd&pwd=qaiu',
// 动态获取url encode 参数
url: apiUrl,
name: '全部文件'
};
const pathStack = [currentDir];
// DOM 元素
const breadcrumbEl = document.getElementById('breadcrumb');
const fileGridEl = document.getElementById('file-grid');
const backBtn = document.getElementById('back-btn');
const folderCountEl = document.getElementById('folder-count');
const fileCountEl = document.getElementById('file-count');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
renderBreadcrumb();
fetchFileList(currentDir.url);
// 返回上一级按钮事件
backBtn.addEventListener('click', goBack);
});
// 渲染面包屑导航
function renderBreadcrumb() {
breadcrumbEl.innerHTML = '';
pathStack.forEach((item, index) => {
const itemEl = document.createElement('div');
itemEl.className = 'breadcrumb-item';
itemEl.textContent = item.name;
if (index < pathStack.length - 1) {
itemEl.addEventListener('click', () => {
// 点击面包屑项返回对应目录
goToDirectory(index);
});
} else {
itemEl.style.cursor = 'default';
itemEl.style.fontWeight = '600';
itemEl.style.color = '#2c3e50';
}
breadcrumbEl.appendChild(itemEl);
// 如果不是最后一个,添加分隔符
if (index < pathStack.length - 1) {
const separator = document.createElement('i');
separator.className = 'fas fa-chevron-right';
breadcrumbEl.appendChild(separator);
}
});
}
// 获取文件列表
async function fetchFileList(url) {
try {
// 显示加载状态
fileGridEl.innerHTML = `
<div class="loading">
<div class="spinner"></div>
</div>
`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code === 200 && data.success) {
renderFileList(data.data);
} else {
throw new Error(data.msg || '获取文件列表失败');
}
} catch (error) {
console.error('获取文件列表失败:', error);
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-exclamation-circle"></i>
<h3>加载失败</h3>
<p>${error.message}</p>
</div>
`;
}
}
// 渲染文件列表
function renderFileList(files) {
fileGridEl.innerHTML = '';
if (!files || files.length === 0) {
fileGridEl.innerHTML = `
<div class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>此文件夹为空</h3>
<p>暂无文件或文件夹</p>
</div>
`;
folderCountEl.textContent = '0';
fileCountEl.textContent = '0';
return;
}
let folderCount = 0;
let fileCount = 0;
files.forEach(file => {
const item = document.createElement('div');
if (file.fileType === 'folder') {
// 文件夹
item.className = 'item folder';
item.innerHTML = `
<div class="item-icon">
<i class="fas fa-folder"></i>
</div>
<div class="item-name">${file.fileName || '未命名文件夹'}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
folderCount++;
// 添加点击事件
item.addEventListener('click', () => {
enterFolder(file);
});
} else {
// 文件
const fileExt = getFileExtension(file.fileName);
const fileTypeInfo = fileTypeIcons[fileExt.toLowerCase()] || fileTypeIcons['default'];
item.className = `item ${fileTypeInfo.type}`;
item.innerHTML = `
<div class="item-icon">
<i class="fas ${fileTypeInfo.icon}"></i>
</div>
<div class="item-name">${file.fileName}</div>
<div class="item-meta">
${file.sizeStr || '0B'} · ${formatDate(file.createTime)}
</div>
`;
fileCount++;
// 添加点击事件
item.addEventListener('click', () => {
handleFileClick(file);
});
}
fileGridEl.appendChild(item);
});
// 更新统计信息
folderCountEl.textContent = folderCount;
fileCountEl.textContent = fileCount;
}
// 获取文件扩展名
function getFileExtension(filename) {
if (!filename) return '';
return filename.split('.').pop();
}
// 进入文件夹
function enterFolder(folder) {
if (!folder.parserUrl) {
alert('无法进入该文件夹,缺少访问链接');
return;
}
const newDir = {
url: folder.parserUrl,
name: folder.fileName || '未命名文件夹'
};
pathStack.push(newDir);
currentDir = newDir;
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 下载文件
function handleFileClick(file) {
if (!file.parserUrl) {
alert('无法操作该文件,缺少必要链接');
return;
}
// 更友好的选择对话框
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
modal.style.zIndex = '1000';
const dialog = document.createElement('div');
dialog.style.backgroundColor = 'white';
dialog.style.padding = '20px';
dialog.style.borderRadius = '8px';
dialog.style.width = '300px';
dialog.style.textAlign = 'center';
dialog.innerHTML = `
<p style="margin-bottom: 20px;">${file.fileName || '未命名文件'}</p>
<div style="display: flex; justify-content: center; gap: 15px;">
<button id="preview-btn" style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
预览文件
</button>
<button id="download-btn" style="padding: 8px 15px; background: #2ecc71; color: white; border: none; border-radius: 4px; cursor: pointer;">
下载文件
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// 预览按钮事件
dialog.querySelector('#preview-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const previewUrl = file.previewUrl || file.parserUrl;
window.open(previewUrl, '_blank');
});
// 下载按钮事件
dialog.querySelector('#download-btn').addEventListener('click', () => {
document.body.removeChild(modal);
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = file.parserUrl;
document.body.appendChild(iframe);
setTimeout(() => {
document.body.removeChild(iframe);
}, 3000);
});
// 点击蒙层关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
}
// 返回上一级
function goBack() {
if (pathStack.length > 1) {
pathStack.pop();
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
}
// 跳转到指定目录
function goToDirectory(index) {
pathStack.splice(index + 1);
currentDir = pathStack[pathStack.length - 1];
fetchFileList(currentDir.url);
renderBreadcrumb();
}
// 格式化日期
function formatDate(dateString) {
if (!dateString) return '未知日期';
try {
const date = new Date(dateString);
return isNaN(date.getTime())
? '未知日期'
: `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
} catch {
return '未知日期';
}
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,367 +1,21 @@
<template>
<div id="app">
<el-row :gutter="20">
<el-card class="box-card">
<div style="text-align: right"><DarkMode/></div>
<div class="demo-basic--circle">
<div class="block" style="text-align: center;">
<img :height="150" src="../public/images/lanzou111.png" alt="lz"></img>
</div>
</div>
<h3 style="text-align: center;">NFD网盘直链解析0.1.8_bate32</h3>
<div class="typo">
<p style="text-align: center;">
<span>
<el-link href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="nofollow">
<u>GitHub</u></el-link>
</span>
<span style="margin-left: 30px">
<el-link href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank"
rel="nofollow"><u>宝塔部署安装教程</u>
</el-link>
</span>
<span style="margin-left: 30px">
<el-link href="https://blog.qaiu.top" target="_blank"
rel="nofollow"><u>QAIU博客</u>
</el-link>
</span></p>
<p><strong>目前支持 </strong>蓝奏云/蓝奏云优享/小飞机盘/123云盘/奶牛快传/移动云云空间/亿方云/文叔叔/QQ邮箱文件中转站</p>
<p>已加入缓存机制, 如果遇到解析出的下载链接失效的情况请及时到<a href="https://github.com/qaiu/netdisk-fast-download/issues">
<u><strong>项目GitHub反馈</strong></u></a></p>
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
<!-- <p>节点2: 成功:{{ node2Info.success }},失败:{{ node2Info.fail }},总数:{{ node2Info.total }}</p>-->
</div>
<hr>
<div class="main" v-loading="isLoading">
<div class="grid-content">
<!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch
v-model="autoReadClipboard"
active-text="自动识别剪切板"
></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
<template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard">
<el-button @click="() => getPaste(1)">读取剪切板</el-button>
</template>
</el-input>
<el-input placeholder="请输入密码" v-model="password" id="url">
<template #prepend>分享密码</template>
</el-input>
<el-input v-show="getLink2" :value="getLink2" id="url">
<template #prepend>智能直链</template>
<template #append>
<el-button v-clipboard:copy="getLink2"
v-clipboard:success="onCopy"
v-clipboard:error="onError">
<el-icon><CopyDocument/></el-icon>
</el-button>
</template>
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px;margin-bottom: 10px" @click="onSubmit">解析测试</el-button>
<el-button style="margin-left: 20px;margin-bottom: 10px" @click="genMd">生成Markdown链接</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getTj">链接信息统计</el-button>
</p>
</div>
<div v-if="respData.code" style="margin-top: 10px">
<strong>解析结果: </strong>
<json-viewer
:value="respData"
:expand-depth=5
copyable
boxed
sort
/>
<a :href="downUrl" v-show="downUrl">点击下载</a>
</div>
<div v-if="mdText" style="text-align: center">
<el-input :value="mdText" readonly>
<template #append>
<el-button v-clipboard:copy="mdText"
v-clipboard:success="onCopy"
v-clipboard:error="onError">
<el-icon><CopyDocument/></el-icon>
</el-button>
</template>
</el-input>
</div>
<div style="text-align: center" v-show="showQrc">
<canvas ref="qrcodeCanvas"></canvas>
<div style="text-align: center"><el-link target="_blank" :href="codeUrl">{{ codeUrl }}</el-link></div>
</div>
<div v-if="tjData.shareLinkInfo">
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
<template slot="extra">
<el-button type="primary" size="small">操作</el-button>
</template>
<el-descriptions-item label="网盘名称">{{ tjData.shareLinkInfo.panName }}</el-descriptions-item>
<el-descriptions-item label="网盘标识">{{ tjData.shareLinkInfo.type }}</el-descriptions-item>
<el-descriptions-item label="分享Key">{{ tjData.shareLinkInfo.shareKey }}</el-descriptions-item>
<el-descriptions-item label="分享链接"> <el-link target="_blank" :href="tjData.shareLinkInfo.shareUrl">{{ tjData.shareLinkInfo.shareUrl }}</el-link></el-descriptions-item>
<el-descriptions-item label="jsonApi链接"> <el-link target="_blank" :href="tjData.apiLink">{{ tjData.apiLink }}</el-link></el-descriptions-item>
<el-descriptions-item label="302下载链接"> <el-link target="_blank" :href="tjData.downLink">{{ tjData.downLink }}</el-link></el-descriptions-item>
<el-descriptions-item label="解析次数">{{ tjData.parserTotal }}</el-descriptions-item>
<el-descriptions-item label="缓存命中次数">{{ tjData.cacheHitTotal }}</el-descriptions-item>
<el-descriptions-item label="总请求次数">{{ tjData.sumTotal }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
</el-row>
<router-view></router-view>
</div>
</template>
<script>
import axios from 'axios'
import QRCode from 'qrcode'
import DarkMode from '@/components/DarkMode'
import parserUrl from './parserUrl1'
export default {
name: 'App',
components: {DarkMode},
data() {
return {
// baseAPI: `${location.protocol}//${location.hostname}:6400`,
baseAPI: `${location.protocol}//${location.host}`,
autoReadClipboard: true, // 开关状态,默认为自动读取
current: {}, // 当前分享
showQrc: false,
codeUrl: '',
mdText: '',
link: "",
password: "",
isLoading: false,
downUrl: null,
select: "lz",
respData: {},
tjData: {},
panList: [
{
name: "蓝奏云",
value: 'lz'
},
{
name: "奶牛快传",
value: 'cow'
},
{
name: "移动云空间",
value: 'ec'
},
{
name: "UC网盘",
value: 'uc',
disabled: true
},
{
name: "小飞机网盘",
value: 'fj'
},
{
name: "360亿方云",
value: 'fc'
},
{
name: "123云盘",
value: 'ye'
},
],
getLink: null,
getLink2: '',
getLinkInfo: null,
node1Info: {},
node2Info: {},
}
},
methods: {
// toggleDark() {
// toggleDark()
// },
check() {
this.mdText = ''
this.showQrc = false
this.respData = {}
this.tjData = {}
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
this.$message.error("请输入有效链接!")
throw new Error('请输入有效链接')
}
},
onSubmit() {
this.check()
this.isLoading = true
this.downUrl = ''
this.respData = {}
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
// this.getLink = `${location.protocol}//${location.host}/api/json/parser?url=${this.link}`
// this.getLink = `${location.protocol}//${location.host}/json/parser`
if (this.password) {
this.getLink2 += `&pwd=${this.password}`
}
axios.get(this.getLink, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
this.respData = response.data
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.downUrl = response.data.data.directLink
} else {
this.$message.error(response.data.msg)
}
this.getInfo()
},
error => {
this.isLoading = false
this.$message.error(error.message)
}
)
},
onCopy() {
this.$message.success('复制成功')
},
onError() {
this.$message.error('复制失败')
},
getInfo() {
// 初始化统计信息
axios.get('/v2/statisticsInfo').then(
response => {
if (response.data.success) {
this.node1Info = response.data.data
}
})
// axios.get('/n2/statisticsInfo').then(
// response => {
// if (response.data.success) {
// this.node2Info = response.data.data
// }
// })
},
genMd() {
this.check()
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.mdText = this.buildMd('快速下载地址',response.data.data.downLink)
} else {
this.$message.error(response.data.msg)
}
});
},
buildMd(title, url) {
return `[${title}](${url})`
},
generateQRCode() {
this.check()
const options = { // 设置二维码的参数,例如大小、边距等
width: 150,
height: 150,
margin: 2
};
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.codeUrl = response.data.data.downLink
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.codeUrl, options, error => {
if (error) console.error(error);
});
this.showQrc = true
} else {
this.$message.error(response.data.msg)
}
});
},
getTj() {
this.check()
axios.get(this.getLinkInfo, {params: {url: this.link, pwd: this.password}}).then(
response => {
this.isLoading = false
if (response.data.code === 200) {
this.$message({
message: response.data.msg,
type: 'success'
})
this.tjData = response.data.data
} else {
this.$message.error(response.data.msg)
}
});
},
async getPaste(v) {
const text = await navigator.clipboard.readText();
console.log('获取到的文本内容是:', text);
let linkInfo = parserUrl.parseLink(text);
let pwd = parserUrl.parsePwd(text) || '';
if (linkInfo.link) {
if(linkInfo.link !== this.link || pwd !== this.password ) {
this.password = pwd;
this.link = linkInfo.link;
this.getLink2 = `${this.baseAPI}/parser?url=${this.link}`
if (this.link) this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`);
} else {
v || this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
}
} else {
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
}
},
},
mounted() {
this.getLinkInfo = `${this.baseAPI}/v2/linkInfo`
this.getLink = `${this.baseAPI}/json/parser`
let item = window.localStorage.getItem("autoReadClipboard");
if (item) {
this.autoReadClipboard = (item === 'true');
}
this.getInfo()
// 页面首次加载时,根据开关状态判断是否读取剪切板
if (this.autoReadClipboard) {
this.getPaste()
}
// 当文档获得焦点时触发
window.addEventListener('focus', () => {
if (this.autoReadClipboard) {
this.getPaste()
}
});
},
watch: {
autoReadClipboard(val) {
window.localStorage.setItem("autoReadClipboard", val)
}
}
name: 'App'
}
</script>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
@@ -371,62 +25,12 @@ export default {
padding: 1em;
max-width: 900px;
}
body:before {
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: .3;
z-index: -1;
position: fixed;
nav ul {
list-style: none;
padding: 0;
}
.grid-content {
margin-top: 1em;
border-radius: 4px;
min-height: 50px;
}
.el-select .el-input {
width: 130px;
}
.box-card {
margin-top: 4em !important;
margin-bottom: 4em !important;
opacity: .8;
}
@media screen and (max-width: 700px) {
.box-card {
margin-top: 1em !important;
margin-bottom: 1em !important;
}
}
.download h3 {
margin-top: 2em;
}
.download button {
margin-right: 0.5em;
margin-left: 0.5em;
}
.typo {
text-align: left;
}
.typo a {
color: #0077ff;
}
hr {
height: 10px;
margin-bottom: .8em;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, .12);
nav li {
display: inline;
margin-right: 15px;
}
</style>

View File

@@ -11,14 +11,18 @@
</template>
<script setup>
import { ref,watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { useDark, useToggle } from '@vueuse/core'
/** 引入Element-Plus图标 */
import { Sunny, Moon } from '@element-plus/icons-vue'
defineOptions({
name: 'DarkMode'
})
// 定义事件
const emit = defineEmits(['theme-change'])
/** 切换模式 */
const isDark = useDark({})
@@ -30,8 +34,32 @@ if (item) {
}
/** 是否切换为暗黑模式 */
const darkMode = ref(item)
watch(darkMode, (newValue) => {
console.log(`darkMode: ${newValue}`)
window.localStorage.setItem("darkMode", newValue);
// 发射主题变化事件
emit('theme-change', newValue)
// 应用主题到body
if (newValue) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
})
onMounted(() => {
// 初始化时发射当前主题状态
emit('theme-change', darkMode.value)
// 应用初始主题
if (darkMode.value) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
}
})
</script>

View File

@@ -0,0 +1,993 @@
<template>
<div class="main-container">
<div class="directory-tree" :class="{ 'dark-theme': isDarkTheme }">
<template v-if="viewMode === 'pane'">
<!-- 窗格模式原有 -->
<div class="breadcrumb">
<div
v-for="(item, index) in pathStack"
:key="index"
class="breadcrumb-item"
:class="{ 'active': index === pathStack.length - 1 }"
@click="goToDirectory(index)"
>
<i class="fas fa-folder" v-if="index === 0"></i>
<i class="fas fa-chevron-right" v-else-if="index > 0"></i>
{{ item.name }}
</div>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in currentFileList"
:key="file.fileName"
class="file-item"
:class="getFileTypeClass(file)"
@click="handleFileClick(file)"
>
<div class="file-icon">
<i :class="getFileIcon(file)"></i>
</div>
<div class="file-name">{{ file.fileName }}</div>
<div class="file-meta">
{{ file.sizeStr || '0B' }} · {{ formatDate(file.createTime) }}
</div>
</div>
<div v-if="!loading && (!currentFileList || currentFileList.length === 0)" class="empty-state">
<i class="fas fa-folder-open"></i>
<h3>此文件夹为空</h3>
<p>暂无文件或文件夹</p>
</div>
</div>
<div class="action-bar">
<el-button
type="primary"
@click="goBack"
:disabled="pathStack.length <= 1"
icon="el-icon-arrow-left"
>
返回上一级
</el-button>
<div class="stats">
<span class="stat-item">
<i class="fas fa-folder"></i> {{ folderCount }} 个文件夹
</span>
<span class="stat-item">
<i class="fas fa-file"></i> {{ fileCount }} 个文件
</span>
</div>
</div>
</template>
<template v-else-if="viewMode === 'tree'">
<div class="content-card">
<splitpanes class="split-theme custom-splitpanes" style="height:100%;">
<pane>
<div class="tree-sidebar">
<el-tree
:data="treeData"
:props="treeProps"
node-key="id"
lazy
:load="loadNode"
highlight-current
@node-click="onNodeClick"
:default-expand-all="false"
:default-expanded-keys="['root']"
:render-content="renderContent"
style="background:transparent;"
/>
</div>
</pane>
<pane>
<div class="tree-content">
<div v-if="selectedNode">
<div class="file-detail-icon-wrap">
<i :class="getFileIcon(selectedNode)" class="file-detail-icon"></i>
</div>
<h4>{{ selectedNode.fileName }}</h4>
<div v-if="selectedNode.fileType === 'folder'">
<ul>
<li v-for="file in selectedNode.children || []" :key="file.id">
<i :class="getFileIcon(file)"></i> {{ file.fileName }}
</li>
</ul>
</div>
<div v-else>
<p>类型: {{ getFileTypeClass(selectedNode) }}</p>
<p>大小: {{ selectedNode.sizeStr || '0B' }}</p>
<p>创建时间: {{ formatDate(selectedNode.createTime) }}</p>
<!-- 文件详情区下载按钮 -->
<el-button v-if="selectedNode && selectedNode.parserUrl" @click="previewFile(selectedNode)">打开</el-button>
<a
v-if="selectedNode && selectedNode.parserUrl"
:href="selectedNode.parserUrl"
download
target="_blank"
class="el-button el-button--success"
style="margin-left: 8px;"
>
下载
</a>
</div>
</div>
<div v-else style="color: #888;">请选择左侧文件或文件夹</div>
</div>
</pane>
</splitpanes>
</div>
</template>
<!-- 文件操作对话框窗格模式下 -->
<el-dialog
v-if="viewMode === 'pane'"
title="文件操作"
v-model="fileDialogVisible"
width="400px"
:before-close="closeFileDialog"
>
<div class="file-dialog-content">
<p><strong>{{ selectedFile?.fileName || '未命名文件' }}</strong></p>
<p class="file-info">
大小: {{ selectedFile?.sizeStr || '0B' }}<br>
创建时间: {{ formatDate(selectedFile?.createTime) }}
</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="previewFile(selectedFile)">打开</el-button>
<!-- 弹窗下载按钮 -->
<a
v-if="selectedFile && selectedFile.parserUrl"
:href="selectedFile.parserUrl"
download
target="_blank"
class="el-button el-button--success"
style="margin-left: 8px;"
>
下载
</a>
</span>
</el-dialog>
<div v-if="isPreviewing" class="preview-mask">
<div class="preview-toolbar">
<el-button size="small" @click="closePreview">关闭预览</el-button>
<el-button size="small" type="primary" @click="openPreviewInNewTab">新窗口打开</el-button>
</div>
<iframe :src="previewUrl" frameborder="0" class="preview-iframe"></iframe>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { ElTree } from 'element-plus'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import fileTypeUtils from '@/utils/fileTypeUtils'
export default {
name: 'DirectoryTree',
components: { ElTree, Splitpanes, Pane },
props: {
fileList: {
type: Array,
default: () => []
},
shareUrl: {
type: String,
required: true
},
password: {
type: String,
default: ''
},
viewMode: {
type: String,
default: 'pane' // 'pane' or 'tree'
}
},
data() {
return {
loading: false,
pathStack: [{ name: '全部文件', url: '' }],
currentFileList: [],
fileDialogVisible: false,
selectedFile: null,
isDarkTheme: false,
initialized: false,
// 文件树模式相关
treeData: [],
selectedNode: null,
isPreviewing: false,
previewUrl: '',
treeProps: {
label: 'fileName',
children: 'children',
isLeaf: 'isLeaf'
}
}
},
computed: {
folderCount() {
return this.currentFileList.filter(file => file.fileType === 'folder').length
},
fileCount() {
return this.currentFileList.filter(file => file.fileType !== 'folder').length
}
},
watch: {
fileList: {
immediate: true,
handler(newList) {
// 根节点children为当前目录下所有文件/文件夹
this.treeData = [
{
id: 'root',
fileName: '全部文件',
fileType: 'folder',
children: (newList || []).map(item => ({
...item,
isLeaf: item.fileType !== 'folder'
})),
isLeaf: false
}
]
this.currentFileList = newList
}
}
},
methods: {
...fileTypeUtils,
// 构建API URL
buildApiUrl() {
const baseUrl = `${window.location.origin}/v2/getFileList`
const params = new URLSearchParams({
url: this.shareUrl
})
if (this.password) {
params.append('pwd', this.password)
}
return `${baseUrl}?${params.toString()}`
},
// 文件树与窗格同源:直接返回当前目录数据
buildTree(list) {
return list || []
},
// 懒加载子节点
loadNode(node, resolve) {
if (node.level === 0) {
// 根节点
resolve(this.treeData[0].children)
} else if (node.data.fileType === 'folder' && node.data.parserUrl) {
axios.get(node.data.parserUrl).then(res => {
if (res.data.code === 200) {
const children = (res.data.data || []).map(item => ({
...item,
isLeaf: item.fileType !== 'folder'
}))
resolve(children)
} else {
resolve([])
}
}).catch(() => resolve([]))
} else {
resolve([])
}
},
onNodeClick(data) {
this.selectedNode = data
},
// 处理文件点击
handleFileClick(file) {
console.log('点击文件', file, this.viewMode)
if (file.fileType === 'folder') {
this.enterFolder(file)
} else if (this.viewMode === 'pane') {
this.selectedFile = file
this.fileDialogVisible = true
}
},
// 进入文件夹
async enterFolder(folder) {
if (!folder.parserUrl) {
this.$message.error('无法进入该文件夹,缺少访问链接')
return
}
try {
this.loading = true
const response = await axios.get(folder.parserUrl)
if (response.data.code === 200) {
const newDir = {
url: folder.parserUrl,
name: folder.fileName || '未命名文件夹'
}
this.pathStack.push(newDir)
this.currentFileList = response.data.data || []
} else {
this.$message.error(response.data.msg || '获取文件夹内容失败')
}
} catch (error) {
console.error('进入文件夹失败:', error)
this.$message.error('进入文件夹失败')
} finally {
this.loading = false
}
},
goBack() {
if (this.pathStack.length > 1) {
this.pathStack.pop()
this.loadCurrentDirectory()
}
},
goToDirectory(index) {
this.pathStack.splice(index + 1)
this.loadCurrentDirectory()
},
async loadCurrentDirectory() {
const currentDir = this.pathStack[this.pathStack.length - 1]
if (!currentDir.url) {
this.currentFileList = this.fileList
return
}
try {
this.loading = true
const response = await axios.get(currentDir.url)
if (response.data.code === 200) {
this.currentFileList = response.data.data || []
} else {
this.$message.error(response.data.msg || '加载目录失败')
}
} catch (error) {
console.error('加载目录失败:', error)
this.$message.error('加载目录失败')
} finally {
this.loading = false
}
},
// 预览文件
previewFile(file) {
if (file?.previewUrl || file?.parserUrl) {
this.previewUrl = file.previewUrl || file.parserUrl
this.isPreviewing = true
} else {
this.$message.warning('该文件暂无预览链接')
}
this.closeFileDialog()
},
// 下载文件
downloadFile(file) {
if (file?.parserUrl) {
const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = file.parserUrl
document.body.appendChild(iframe)
setTimeout(() => {
document.body.removeChild(iframe)
}, 1000)
this.$message.success('开始下载文件')
} else {
this.$message.warning('该文件暂无下载链接')
}
this.closeFileDialog()
},
closeFileDialog() {
this.fileDialogVisible = false
this.selectedFile = null
},
closePreview() {
this.isPreviewing = false
this.previewUrl = ''
},
openPreviewInNewTab() {
if (this.previewUrl) {
window.open(this.previewUrl, '_blank')
}
},
formatDate(timestamp) {
if (!timestamp) return '未知时间'
const date = new Date(timestamp)
return date.toLocaleString('zh-CN')
},
checkTheme() {
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
document.documentElement.classList.contains('dark-theme')
},
renderContent(h, { node, data, store }) {
const isFolder = data.fileType === 'folder'
return h('div', {
class: 'custom-tree-node'
}, [
h('i', {
class: [this.getFileIcon(data), { 'folder-icon': isFolder, 'file-icon': !isFolder }]
}),
h('span', {
class: ['node-label', { 'folder-text': isFolder, 'file-text': !isFolder }]
}, node.label)
])
}
},
mounted() {
this.checkTheme()
this.initialized = true
// 监听主题变化
this._observer = new MutationObserver(() => {
this.checkTheme()
})
this._observer.observe(document.body, {
attributes: true,
attributeFilter: ['class']
})
},
beforeUnmount() {
if (this._observer) {
this._observer.disconnect()
}
}
}
</script>
<style>
html, body, #app, .main-container, .directory-tree, .content-card {
/* overflow: hidden; */
/* overflow: auto; */
/* position: relative; */
}
.main-container {
height: 100%;
display: flex;
flex-direction: column;
}
.directory-tree {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
overflow: hidden;
transition: all 0.3s ease;
}
.directory-tree.dark-theme {
background: #2d2d2d;
color: #ffffff;
}
.breadcrumb {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #eaeaea;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.dark-theme .breadcrumb {
background: #404040;
border-bottom-color: #555555;
}
.breadcrumb-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: #7f8c8d;
cursor: pointer;
transition: color 0.2s;
margin-right: 8px;
}
.breadcrumb-item:hover {
color: #3498db;
}
.breadcrumb-item.active {
color: #2c3e50;
font-weight: 600;
}
.dark-theme .breadcrumb-item.active {
color: #ffffff;
}
.breadcrumb-item i {
margin: 0 8px;
font-size: 0.8rem;
color: #bdc3c7;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
padding: 12px;
min-height: 200px;
}
.file-item {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
cursor: pointer;
text-align: center;
padding: 10px 4px;
min-height: 80px;
border: 2px solid transparent;
}
.dark-theme .file-item {
background: #404040;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2);
}
.file-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
.dark-theme .file-item:hover {
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.file-icon {
font-size: 2.2rem;
margin-bottom: 8px;
transition: transform 0.3s;
}
.file-item:hover .file-icon {
transform: scale(1.1);
}
.folder .file-icon {
color: #3498db;
}
.image .file-icon {
color: #e74c3c;
}
.document .file-icon {
color: #f39c12;
}
.archive .file-icon {
color: #9b59b6;
}
.audio .file-icon {
color: #1abc9c;
}
.video .file-icon {
color: #d35400;
}
.code .file-icon {
color: #27ae60;
}
.file-name {
font-weight: 500;
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
margin-bottom: 4px;
}
.file-meta {
font-size: 0.75rem;
color: #95a5a6;
}
.dark-theme .file-meta {
color: #bdc3c7;
}
.empty-state {
text-align: center;
padding: 30px 10px;
color: #7f8c8d;
grid-column: 1 / -1;
}
.dark-theme .empty-state {
color: #bdc3c7;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 10px;
color: #bdc3c7;
}
.dark-theme .empty-state i {
color: #555555;
}
.empty-state h3 {
font-size: 1.1rem;
margin-bottom: 6px;
color: #2c3e50;
}
.dark-theme .empty-state h3 {
color: #ffffff;
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 10px 12px;
background: #f8f9fa;
border-top: 1px solid #eaeaea;
}
.dark-theme .action-bar {
background: #404040;
border-top-color: #555555;
}
.stats {
display: flex;
align-items: center;
gap: 10px;
color: #7f8c8d;
font-size: 0.85rem;
}
.dark-theme .stats {
color: #bdc3c7;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.file-dialog-content {
text-align: center;
}
.file-info {
color: #7f8c8d;
font-size: 0.9rem;
margin-top: 10px;
}
.dark-theme .file-info {
color: #bdc3c7;
}
.tree-layout {
display: flex;
height: 500px;
}
.tree-sidebar {
width: 220px;
background: #f8f9fa;
border-right: 1px solid #eaeaea;
overflow-y: auto;
}
.directory-tree.dark-theme .tree-sidebar {
background: #232323;
border-right: 1px solid #404040;
}
.file-tree-root, .tree-node ul {
list-style: none;
padding-left: 12px;
margin: 0;
}
.tree-node {
margin-bottom: 2px;
}
.tree-node.selected > .tree-node-label {
background: #e6f7ff;
color: #409eff;
}
.directory-tree.dark-theme .tree-node.selected > .tree-node-label {
background: #333c4d;
color: #4a9eff;
}
.tree-node-label {
cursor: pointer;
padding: 3px 6px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 4px;
transition: background 0.2s;
font-size: 0.95em;
}
.tree-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* 自定义树节点样式 */
.custom-tree-node {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 0;
width: 100%;
}
.custom-tree-node i {
font-size: 16px;
width: 20px;
text-align: center;
color: #606266;
}
.dark-theme .custom-tree-node i {
color: #bdc3c7;
}
.custom-tree-node .node-label {
flex: 1;
font-size: 14px;
color: #303133;
}
.dark-theme .custom-tree-node .node-label {
color: #e1e1e1;
}
/* 文件夹样式 */
.custom-tree-node .folder-icon {
color: #409eff !important;
}
.dark-theme .custom-tree-node .folder-icon {
color: #4a9eff !important;
}
.custom-tree-node .folder-text {
color: #409eff !important;
font-weight: 500;
}
.dark-theme .custom-tree-node .folder-text {
color: #4a9eff !important;
}
/* 文件样式 */
.custom-tree-node .file-icon {
color: #95a5a6 !important;
}
.dark-theme .custom-tree-node .file-icon {
color: #bdc3c7 !important;
}
.custom-tree-node .file-text {
color: #606266 !important;
}
.dark-theme .custom-tree-node .file-text {
color: #e1e1e1 !important;
}
/* 特殊文件类型图标颜色 */
.custom-tree-node i.fa-file-image {
color: #e74c3c !important;
}
.custom-tree-node i.fa-file-pdf {
color: #e74c3c !important;
}
.custom-tree-node i.fa-file-word {
color: #3498db !important;
}
.custom-tree-node i.fa-file-excel {
color: #27ae60 !important;
}
.custom-tree-node i.fa-file-powerpoint {
color: #f39c12 !important;
}
.custom-tree-node i.fa-file-archive {
color: #9b59b6 !important;
}
.custom-tree-node i.fa-file-audio {
color: #1abc9c !important;
}
.custom-tree-node i.fa-file-video {
color: #d35400 !important;
}
.custom-tree-node i.fa-file-code {
color: #27ae60 !important;
}
/* 树节点悬停效果 */
.el-tree-node__content:hover .custom-tree-node {
background-color: #f5f7fa;
border-radius: 4px;
}
.dark-theme .el-tree-node__content:hover .custom-tree-node {
background-color: #2c2c2c;
}
/* 选中节点样式 */
.el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
background-color: #e6f7ff;
border-radius: 4px;
}
.dark-theme .el-tree-node.is-current > .el-tree-node__content .custom-tree-node {
background-color: #333c4d;
}
.preview-mask { position: fixed; z-index: 9999; left: 0; top: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; }
.preview-toolbar { padding: 12px; background: #232323; text-align: right; }
.preview-iframe { flex: 1; width: 100vw; border: none; background: #222; }
.content-card {
min-height: 500px;
height: 100%;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
margin: 0 0 12px 0;
display: flex;
flex-direction: column;
}
.dark-theme .content-card {
background: #232323;
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.split-theme {
flex: 1 1 0;
height: 100%;
}
.tree-sidebar, .tree-content {
width: 100%;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow: auto;
}
.tree-content {
padding: 40px 16px 16px 16px;
align-items: flex-start;
}
.file-detail-icon-wrap {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.file-detail-icon {
font-size: 48px;
color: #409eff;
display: block;
}
.dark-theme .file-detail-icon {
color: #4a9eff;
}
/* splitpanes 拖拽条自定义按钮 */
.custom-splitpanes .splitpanes__splitter {
position: relative;
background: #e0e0e0;
transition: background 0.2s;
touch-action: pan-x pan-y;
}
.custom-splitpanes .splitpanes__splitter:hover {
background: #b3b3b3;
}
.custom-splitpanes .splitpanes__splitter:after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 28px;
height: 28px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border: 1.5px solid #d0d0d0;
z-index: 2;
display: block;
}
.dark-theme .custom-splitpanes .splitpanes__splitter:after {
background: #232323;
border-color: #444;
}
.feedback-bar {
width: 100%;
text-align: right;
padding: 12px 18px 0 0;
}
.feedback-link {
color: #e74c3c;
font-weight: bold;
font-size: 1.08rem;
text-decoration: none;
border: 1px solid #e74c3c;
border-radius: 6px;
padding: 4px 14px;
background: #fff5f5;
transition: background 0.2s, color 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 10px;
}
.feedback-link:first-child { margin-left: 0; }
.feedback-link:hover {
background: #e74c3c;
color: #fff;
}
.dark-theme .feedback-link {
background: #2d2d2d;
color: #ff7675;
border-color: #ff7675;
}
.dark-theme .feedback-link:hover {
background: #ff7675;
color: #232323;
}
.feedback-icon {
font-size: 1.15em;
color: #e74c3c;
margin-right: 2px;
}
.feedback-link:hover .feedback-icon {
color: #fff;
}
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
.dark-theme .feedback-icon {
color: #ff7675;
}
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
@media (max-width: 768px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 8px;
padding: 6px;
}
.file-item {
padding: 6px 2px;
min-height: 60px;
}
.file-icon {
font-size: 1.5rem;
}
}
@media (max-width: 480px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 4px;
}
.action-bar {
flex-direction: column;
gap: 6px;
}
}
</style>

View File

@@ -9,22 +9,24 @@ import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import "vue3-json-viewer/dist/index.css";
import './styles/dark/css-vars.css'
import router from './router/index.js'
window.$vueApp = Vue.createApp(App)
const app = Vue.createApp(App)
app.use(router)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
window.$vueApp.component(key, component)
app.component(key, component)
}
// Import JsonViewer as a Vue.js plugin
window.$vueApp.use(JsonViewer)
window.$vueApp.use(DirectiveExtensions)
app.use(JsonViewer)
app.use(DirectiveExtensions)
// or
// components: {JsonViewer}
window.$vueApp.use(VueClipboard)
window.$vueApp.use(ElementPlus)
window.$vueApp.mount('#app')
app.use(VueClipboard)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import ShowFile from '@/views/ShowFile.vue'
import ShowList from '@/views/ShowList.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/showFile', component: ShowFile },
{ path: '/showList', component: ShowList }
]
const router = createRouter({
history: createWebHistory('/'),
routes
})
export default router

View File

@@ -0,0 +1,164 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: all 0.3s ease;
}
/* 亮色主题 */
body {
background-color: #f5f7fa;
color: #2c3e50;
}
/* 暗色主题 */
body.dark-theme {
background-color: #1a1a1a;
color: #ffffff;
}
/* Element Plus 暗色主题适配 */
.dark-theme .el-card {
background-color: #2d2d2d !important;
border-color: #404040 !important;
color: #ffffff !important;
}
.dark-theme .el-input__inner {
background-color: #404040 !important;
border-color: #555555 !important;
color: #ffffff !important;
}
.dark-theme .el-input__inner::placeholder {
color: #bdc3c7 !important;
}
.dark-theme .el-button {
background-color: #404040 !important;
border-color: #555555 !important;
color: #ffffff !important;
}
.dark-theme .el-button:hover {
background-color: #555555 !important;
}
.dark-theme .el-button--primary {
background-color: #409eff !important;
border-color: #409eff !important;
}
.dark-theme .el-button--primary:hover {
background-color: #66b1ff !important;
}
.dark-theme .el-button--success {
background-color: #67c23a !important;
border-color: #67c23a !important;
}
.dark-theme .el-button--success:hover {
background-color: #85ce61 !important;
}
.dark-theme .el-switch__core {
background-color: #555555 !important;
}
.dark-theme .el-descriptions {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-descriptions__label {
color: #bdc3c7 !important;
}
.dark-theme .el-descriptions__content {
color: #ffffff !important;
}
.dark-theme .el-dialog {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-dialog__title {
color: #ffffff !important;
}
.dark-theme .el-dialog__body {
color: #ffffff !important;
}
.dark-theme .el-message {
background-color: #2d2d2d !important;
color: #ffffff !important;
}
.dark-theme .el-message--success {
background-color: #67c23a !important;
color: #ffffff !important;
}
.dark-theme .el-message--error {
background-color: #f56c6c !important;
color: #ffffff !important;
}
.dark-theme .el-message--warning {
background-color: #e6a23c !important;
color: #ffffff !important;
}
.dark-theme .el-message--info {
background-color: #909399 !important;
color: #ffffff !important;
}
/* 链接颜色 */
.dark-theme a {
color: #4a9eff !important;
}
.dark-theme a:hover {
color: #66b1ff !important;
}
/* 滚动条样式 */
.dark-theme ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.dark-theme ::-webkit-scrollbar-track {
background: #2d2d2d;
}
.dark-theme ::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 4px;
}
.dark-theme ::-webkit-scrollbar-thumb:hover {
background: #666666;
}
/* 选择文本样式 */
.dark-theme ::selection {
background-color: #409eff;
color: #ffffff;
}
.dark-theme ::-moz-selection {
background-color: #409eff;
color: #ffffff;
}

View File

@@ -0,0 +1,85 @@
const fileTypeUtils = {
getFileExtension(filename) {
if (!filename) return ''
return filename.split('.').pop()
},
getFileTypeClass(file) {
if (file.fileType === 'folder') return 'folder'
const ext = this.getFileExtension(file.fileName)
const fileTypes = {
'image': ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'],
'document': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'],
'archive': ['zip', 'rar', '7z', 'tar', 'gz'],
'audio': ['mp3', 'wav', 'ogg', 'flac'],
'video': ['mp4', 'avi', 'mov', 'wmv', 'mkv', 'flv'],
'code': ['html', 'htm', 'css', 'js', 'json', 'php', 'py', 'java', 'c', 'cpp', 'h', 'sh', 'bat', 'md']
}
for (const [type, extensions] of Object.entries(fileTypes)) {
if (extensions.includes(ext.toLowerCase())) {
return type
}
}
return 'document'
},
getFileIcon(file) {
if (file.fileType === 'folder') return 'fas fa-folder'
const ext = this.getFileExtension(file.fileName)
const iconMap = {
'jpg': 'fas fa-file-image', 'jpeg': 'fas fa-file-image', 'png': 'fas fa-file-image',
'gif': 'fas fa-file-image', 'bmp': 'fas fa-file-image', 'svg': 'fas fa-file-image', 'webp': 'fas fa-file-image',
'pdf': 'fas fa-file-pdf', 'doc': 'fas fa-file-word', 'docx': 'fas fa-file-word',
'xls': 'fas fa-file-excel', 'xlsx': 'fas fa-file-excel', 'ppt': 'fas fa-file-powerpoint', 'pptx': 'fas fa-file-powerpoint',
'txt': 'fas fa-file-alt', 'rtf': 'fas fa-file-alt',
'zip': 'fas fa-file-archive', 'rar': 'fas fa-file-archive', '7z': 'fas fa-file-archive',
'tar': 'fas fa-file-archive', 'gz': 'fas fa-file-archive',
'mp3': 'fas fa-file-audio', 'wav': 'fas fa-file-audio', 'ogg': 'fas fa-file-audio', 'flac': 'fas fa-file-audio',
'mp4': 'fas fa-file-video', 'avi': 'fas fa-file-video', 'mov': 'fas fa-file-video',
'wmv': 'fas fa-file-video', 'mkv': 'fas fa-file-video', 'flv': 'fas fa-file-video',
'html': 'fas fa-file-code', 'htm': 'fas fa-file-code', 'css': 'fas fa-file-code',
'js': 'fas fa-file-code', 'json': 'fas fa-file-code', 'php': 'fas fa-file-code',
'py': 'fas fa-file-code', 'java': 'fas fa-file-code', 'c': 'fas fa-file-code',
'cpp': 'fas fa-file-code', 'h': 'fas fa-file-code', 'sh': 'fas fa-file-code',
'bat': 'fas fa-file-code', 'md': 'fas fa-file-code'
}
return iconMap[ext.toLowerCase()] || 'fas fa-file'
},
extractFileNameAndExt(url) {
if (!url) return { name: '', ext: '' }
const filenameParams = [
'response-content-disposition', 'filename', 'filename*', 'fn', 'fname', 'download_name'
];
let name = null;
try {
const u = new URL(url, window.location.origin);
for (const param of filenameParams) {
const value = u.searchParams.get(param);
if (value) {
if (param === 'response-content-disposition') {
const match = value.match(/filename\*?=(.*'')?(?<FN>.*)/i);
name = match && match.groups && match.groups['FN'] ? match.groups['FN'] : value;
} else {
name = value;
}
break;
}
}
if (name) {
name = decodeURIComponent(name).replace(/['"]/g, '');
} else {
const decodedUrl = decodeURIComponent(url);
const paths = decodedUrl.split('/');
name = paths[paths.length - 1].split('?')[0];
}
let ext = '';
if (name) {
const spl = name.split('.');
ext = spl.length > 1 ? spl[spl.length - 1].toLowerCase() : '';
}
return { name, ext };
} catch {
return { name: '', ext: '' }
}
}
}
export default fileTypeUtils

View File

@@ -0,0 +1,793 @@
<template>
<div id="app" :class="{ 'dark-theme': isDarkMode }">
<!-- <el-dialog
v-model="showRiskDialog"
title="使用本网站您应改同意"
width="300px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
center
>
<div style="font-size:1.08em;line-height:1.8;">
请勿在本平台分享传播任何违法内容包括但不限于<br>
违规视频游戏外挂侵权资源涉政涉黄等<br>
</div>
<template #footer>
<el-button type="primary" @click="ackRisk">我知道了</el-button>
</template>
</el-dialog> -->
<!-- 顶部反馈栏小号灰色无红边框 -->
<div class="feedback-bar">
<a href="https://github.com/qaiu/lz.qaiu.top/issues" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-bug feedback-icon"></i>
反馈
</a>
<a href="https://github.com/qaiu/lz.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fab fa-github feedback-icon"></i>
GitHub
</a>
<a href="https://blog.qaiu.top" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-blog feedback-icon"></i>
博客
</a>
<a href="https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng" target="_blank" rel="noopener" class="feedback-link mini">
<i class="fas fa-server feedback-icon"></i>
部署
</a>
</div>
<el-row :gutter="20">
<el-card class="box-card">
<div style="text-align: right">
<DarkMode @theme-change="handleThemeChange" />
</div>
<div class="demo-basic--circle">
<div class="block" style="text-align: center;">
<img :height="150" src="../../public/images/lanzou111.png" alt="lz"></img>
</div>
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.1.9_bate6</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间亿方云文叔叔QQ邮箱文件中转站等</div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div>
</div>
<div class="typo">
<p>节点1: 回源请求数:{{ node1Info.parserTotal }}, 缓存请求数:{{ node1Info.cacheTotal }}, 总数:{{ node1Info.total }}</p>
</div>
<hr>
<div class="main" v-loading="isLoading">
<div class="grid-content">
<!-- 开关按钮控制是否自动读取剪切板 -->
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
<template #prepend>分享链接</template>
<template #append v-if="!autoReadClipboard">
<el-button @click="getPaste(true)">读取剪切板</el-button>
</template>
</el-input>
<el-input placeholder="请输入密码" v-model="password" id="url">
<template #prepend>分享密码</template>
</el-input>
<el-input v-show="directLink" :value="directLink" id="url">
<template #prepend>智能直链</template>
<template #append>
<el-button v-clipboard:copy="directLink" v-clipboard:success="onCopy" v-clipboard:error="onError">
<el-icon><CopyDocument /></el-icon>
</el-button>
</template>
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button>
<el-button style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
</p>
</div>
<!-- 解析结果 -->
<div v-if="parseResult.code" style="margin-top: 10px">
<strong>解析结果: </strong>
<json-viewer :value="parseResult" :expand-depth="5" copyable boxed sort />
<!-- 文件信息美化展示区 -->
<div v-if="downloadUrl" class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
</div>
<div class="file-meta-row" v-if="parseResult.data?.downloadShortUrl">
<span class="file-meta-label">下载短链</span>
<a :href="parseResult.data.downloadShortUrl" target="_blank" class="file-meta-link">{{ parseResult.data.downloadShortUrl }}</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件预览</span>
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="file-meta-link">点击预览</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件名</span>{{ extractFileNameAndExt(downloadUrl).name }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件类型</span>{{ getFileTypeClass({ fileName: extractFileNameAndExt(downloadUrl).name }) }}
</div>
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
<span class="file-meta-label">文件大小</span>{{ parseResult.data.sizeStr }}
</div>
</div>
</div>
<!-- Markdown链接 -->
<div v-if="markdownText" style="text-align: center">
<el-input :value="markdownText" readonly>
<template #append>
<el-button v-clipboard:copy="markdownText" v-clipboard:success="onCopy" v-clipboard:error="onError">
<el-icon><CopyDocument /></el-icon>
</el-button>
</template>
</el-input>
</div>
<!-- 二维码 -->
<div style="text-align: center" v-show="showQRCode">
<canvas ref="qrcodeCanvas"></canvas>
<div style="text-align: center">
<el-link target="_blank" :href="qrCodeUrl">{{ qrCodeUrl }}</el-link>
</div>
</div>
<!-- 统计信息 -->
<div v-if="statisticsData.shareLinkInfo">
<el-descriptions class="margin-top" title="分享详情" :column="1" border>
<template slot="extra">
<el-button type="primary" size="small">操作</el-button>
</template>
<el-descriptions-item label="网盘名称">{{ statisticsData.shareLinkInfo.panName }}</el-descriptions-item>
<el-descriptions-item label="网盘标识">{{ statisticsData.shareLinkInfo.type }}</el-descriptions-item>
<el-descriptions-item label="分享Key">{{ statisticsData.shareLinkInfo.shareKey }}</el-descriptions-item>
<el-descriptions-item label="分享链接">
<el-link target="_blank" :href="statisticsData.shareLinkInfo.shareUrl">{{ statisticsData.shareLinkInfo.shareUrl }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="jsonApi链接">
<el-link target="_blank" :href="statisticsData.apiLink">{{ statisticsData.apiLink }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="302下载链接">
<el-link target="_blank" :href="statisticsData.downLink">{{ statisticsData.downLink }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="解析次数">{{ statisticsData.parserTotal }}</el-descriptions-item>
<el-descriptions-item label="缓存命中次数">{{ statisticsData.cacheHitTotal }}</el-descriptions-item>
<el-descriptions-item label="总请求次数">{{ statisticsData.sumTotal }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 目录树组件 -->
<div v-if="showDirectoryTree" class="directory-tree-container">
<div style="margin-bottom: 10px; text-align: right;">
<el-radio-group v-model="directoryViewMode" size="small">
<el-radio-button label="pane">窗格</el-radio-button>
<el-radio-button label="tree">文件树</el-radio-button>
</el-radio-group>
</div>
<DirectoryTree
:file-list="directoryData"
:share-url="link"
:password="password"
:view-mode="directoryViewMode"
@file-click="handleFileClick"
/>
</div>
</div>
</el-card>
</el-row>
<!-- 文件解析结果区下方加分享按钮 -->
<div v-if="parseResult.code && downloadUrl" style="margin-top: 10px; text-align: right;">
<el-button type="primary" @click="copyShowFileLink">分享文件直链</el-button>
</div>
<!-- 目录解析结果区下方加分享按钮 -->
<div v-if="showDirectoryTree && directoryData.length" style="margin-top: 10px; text-align: right;">
<el-button type="primary" @click="copyShowListLink">分享目录直链</el-button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import QRCode from 'qrcode'
import DarkMode from '@/components/DarkMode'
import DirectoryTree from '@/components/DirectoryTree'
import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
export default {
name: 'App',
components: { DarkMode, DirectoryTree },
mixins: [fileTypeUtils],
data() {
return {
baseAPI: `${location.protocol}//${location.host}`,
autoReadClipboard: true,
isDarkMode: false,
isLoading: false,
// 输入数据
link: "",
password: "",
// 解析结果
parseResult: {},
downloadUrl: null,
directLink: '',
previewBaseUrl,
// 功能结果
markdownText: '',
showQRCode: false,
qrCodeUrl: '',
statisticsData: {},
// 目录树
showDirectoryTree: false,
directoryData: [],
// 统计信息
node1Info: {},
node2Info: {},
hasWarnedNoLink: false,
directoryViewMode: 'pane', // 新增,目录树展示模式
hasClipboardSuccessTip: false, // 新增,聚焦期间只提示一次
showRiskDialog: false,
baseUrl: location.origin
}
},
methods: {
// 主题切换
handleThemeChange(isDark) {
this.isDarkMode = isDark
},
// 验证输入
validateInput() {
this.clearResults()
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
this.$message.error("请输入有效链接!")
throw new Error('请输入有效链接')
}
},
// 清除结果
clearResults() {
this.parseResult = {}
this.downloadUrl = null
this.markdownText = ''
this.showQRCode = false
this.statisticsData = {}
this.showDirectoryTree = false
this.directoryData = []
},
// 统一API调用
async callAPI(endpoint, params = {}) {
try {
this.isLoading = true
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
if (response.data.code === 200) {
// this.$message.success(response.data.msg || '操作成功')
return response.data
} else {
throw new Error(response.data.msg || '操作失败')
}
} catch (error) {
this.$message.error(error.message || '网络错误')
throw error
} finally {
this.isLoading = false
}
},
// 文件解析
async parseFile() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/json/parser', params)
this.parseResult = result
this.downloadUrl = result.data?.directLink
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
this.$message.success('文件解析成功!')
} catch (error) {
console.error('文件解析失败:', error)
}
},
// 目录解析
async parseDirectory() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
const data = result.data
// 检查是否支持目录解析
const supportedPans = ["iz", "lz", "fj", "ye"]
if (!supportedPans.includes(data.shareLinkInfo.type)) {
this.$message.error("当前网盘不支持目录解析")
return
}
// 获取目录数据
const directoryResult = await this.callAPI('/v2/getFileList', params)
this.directoryData = directoryResult.data || []
this.showDirectoryTree = true
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
} catch (error) {
console.error('目录解析失败:', error)
}
},
// 生成Markdown
async generateMarkdown() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.markdownText = this.buildMarkdown('快速下载地址', result.data.downLink)
this.$message.success('Markdown生成成功')
} catch (error) {
console.error('生成Markdown失败:', error)
}
},
// 生成二维码
async generateQRCode() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.qrCodeUrl = result.data.downLink
const options = {
width: 150,
height: 150,
margin: 2
}
QRCode.toCanvas(this.$refs.qrcodeCanvas, this.qrCodeUrl, options, error => {
if (error) console.error(error)
})
this.showQRCode = true
this.$message.success('二维码生成成功!')
} catch (error) {
console.error('生成二维码失败:', error)
}
},
// 获取统计信息
async getStatistics() {
try {
this.validateInput()
const params = { url: this.link }
if (this.password) params.pwd = this.password
const result = await this.callAPI('/v2/linkInfo', params)
this.statisticsData = result.data
this.$message.success('统计信息获取成功!')
} catch (error) {
console.error('获取统计信息失败:', error)
}
},
// 构建Markdown链接
buildMarkdown(title, url) {
return `[${title}](${url})`
},
// 复制成功
onCopy() {
this.$message.success('复制成功')
},
// 复制失败
onError() {
this.$message.error('复制失败')
},
// 文件点击处理
handleFileClick(file) {
if (file.parserUrl) {
window.open(file.parserUrl, '_blank')
} else {
this.$message.warning('该文件暂无下载链接')
}
},
// 获取剪切板内容
async getPaste(isManual = false) {
try {
const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text)
const linkInfo = parserUrl.parseLink(text)
const pwd = parserUrl.parsePwd(text) || ''
if (linkInfo.link) {
if (linkInfo.link !== this.link || pwd !== this.password) {
this.password = pwd
this.link = linkInfo.link
this.directLink = `${this.baseAPI}/parser?url=${this.link}${this.password ? `&pwd=${this.password}` : ''}`
// 聚焦期间只提示一次
if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
this.hasClipboardSuccessTip = true
}
} else {
this.$message.warning(`[${linkInfo.name}]分享信息无变化`)
}
this.hasWarnedNoLink = false // 有效链接后重置
} else {
if (isManual || !this.hasWarnedNoLink) {
this.$message.warning("未能提取到分享链接, 该分享可能尚未支持, 你可以复制任意网盘/音乐App的分享到该页面, 系统智能识别")
this.hasWarnedNoLink = true
}
}
} catch (error) {
console.error('读取剪切板失败:', error)
this.$message.error('读取剪切板失败,请检查浏览器权限')
}
},
// 获取统计信息
async getInfo() {
try {
const response = await axios.get('/v2/statisticsInfo')
if (response.data.success) {
this.node1Info = response.data.data
}
} catch (error) {
console.error('获取统计信息失败:', error)
}
},
// 新增切换目录树展示模式方法
setDirectoryViewMode(mode) {
this.directoryViewMode = mode
},
// 文件名和类型提取方法(复用 DirectoryTree 的静态方法)
extractFileNameAndExt(url) {
return fileTypeUtils.extractFileNameAndExt(url)
},
getFileTypeClass(file) {
return fileTypeUtils.getFileTypeClass(file)
},
ackRisk() {
window.localStorage.setItem('nfd_risk_ack', '1')
this.showRiskDialog = false
},
copyShowFileLink() {
const url = `${this.baseUrl}/showFile?url=${encodeURIComponent(this.downloadUrl)}`
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('文件分享链接已复制!')
})
},
copyShowListLink() {
const url = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('目录分享链接已复制!')
})
}
},
mounted() {
// 从localStorage读取设置
const savedAutoRead = window.localStorage.getItem("autoReadClipboard")
if (savedAutoRead !== null) {
this.autoReadClipboard = savedAutoRead === 'true'
}
// 获取初始统计信息
this.getInfo()
// 自动读取剪切板
if (this.autoReadClipboard) {
this.getPaste()
}
// 监听窗口焦点事件
window.addEventListener('focus', () => {
if (this.autoReadClipboard) {
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
this.getPaste()
}
})
// 首次打开页面弹出风险提示
if (!window.localStorage.getItem('nfd_risk_ack')) {
this.showRiskDialog = true
}
},
watch: {
downloadUrl(val) {
if (!val) {
this.$router.push('/')
}
},
autoReadClipboard(val) {
window.localStorage.setItem("autoReadClipboard", val)
}
}
}
</script>
<style>
body {
background-color: #f5f7fa;
color: #2c3e50;
margin: 0;
padding: 0;
}
body.dark-theme {
background-color: #181818;
color: #ffffff;
}
#app {
/* 不设置 background-color */
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: auto;
padding: 1em;
max-width: 900px;
}
#app.dark-theme {
/* 不设置 background-color */
color: #ffffff;
}
.box-card {
margin-top: 4em !important;
margin-bottom: 4em !important;
opacity: 1 !important; /* 只要不透明 */
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
border: none;
}
#app.dark-theme .box-card {
background: #232323 !important;
color: #fff !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: none;
}
@media screen and (max-width: 700px) {
.box-card {
margin-top: 1em !important;
margin-bottom: 1em !important;
}
}
.grid-content {
margin-top: 1em;
border-radius: 4px;
min-height: 50px;
}
.el-select .el-input {
width: 130px;
}
.directory-tree-container {
margin-top: 20px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafafa;
}
#app.dark-theme .directory-tree-container {
background-color: #2d2d2d;
border-color: #404040;
}
.download h3 {
margin-top: 2em;
}
.download button {
margin-right: 0.5em;
margin-left: 0.5em;
}
.typo {
text-align: left;
}
.typo a {
color: #0077ff;
}
#app.dark-theme .typo a {
color: #4a9eff;
}
hr {
height: 10px;
margin-bottom: .8em;
border: none;
border-bottom: 1px solid rgba(0, 0, 0, .12);
}
#app.dark-theme hr {
border-bottom-color: rgba(255, 255, 255, .12);
}
.feedback-bar {
width: 100%;
text-align: right;
padding: 10px 10px 0 0;
}
.feedback-link {
color: #888;
font-weight: 500;
font-size: 0.98rem;
text-decoration: none;
border: none;
border-radius: 5px;
padding: 2px 10px;
background: transparent;
transition: background 0.2s, color 0.2s;
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 6px;
}
.feedback-link:first-child { margin-left: 0; }
.feedback-link:hover {
background: #f0f0f0;
color: #333;
}
.dark-theme .feedback-link {
background: transparent;
color: #bbb;
border: none;
}
.dark-theme .feedback-link:hover {
background: #333;
color: #fff;
}
.feedback-link.mini {
font-size: 0.92rem;
padding: 2px 8px;
border-radius: 4px;
}
.feedback-icon {
font-size: 1em;
color: #888;
margin-right: 2px;
}
.feedback-link:hover .feedback-icon {
color: #333;
}
.feedback-link:nth-child(2) .feedback-icon { color: #333; }
.feedback-link:nth-child(3) .feedback-icon { color: #f39c12; }
.dark-theme .feedback-icon {
color: #bbb;
}
.dark-theme .feedback-link:nth-child(2) .feedback-icon { color: #fff; }
.dark-theme .feedback-link:nth-child(3) .feedback-icon { color: #f7ca77; }
.feedback-link:nth-child(4) .feedback-icon { color: #409eff; }
.dark-theme .feedback-link:nth-child(4) .feedback-icon { color: #4a9eff; }
.project-intro {
margin: 0 auto 18px auto;
max-width: 700px;
text-align: center;
color: #888;
font-size: 1.02rem;
}
.intro-title {
font-size: 1.18rem;
font-weight: bold;
margin-bottom: 4px;
color: #666;
}
.intro-desc {
color: #888;
font-size: 0.98rem;
line-height: 1.7;
}
.dark-theme .project-intro, .dark-theme .intro-title, .dark-theme .intro-desc {
color: #bbb;
}
.dark-theme .intro-title {
color: #eee;
}
.file-meta-info-card {
margin: 18px auto 0 auto;
max-width: 600px;
background: #f8f9fa;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 18px 24px 12px 24px;
font-size: 1.02rem;
color: #333;
}
#app.dark-theme .file-meta-info-card {
background: #232323;
color: #eee;
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
}
.file-meta-row {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 1.01em;
}
.file-meta-label {
min-width: 90px;
color: #888;
font-weight: 500;
margin-right: 8px;
}
#app.dark-theme .file-meta-label {
color: #bbb;
}
.file-meta-link {
color: #409eff;
word-break: break-all;
text-decoration: underline;
}
#app.dark-theme .file-meta-link {
color: #4a9eff;
}
#app.dark-theme .jv-container {
background: #232323 !important;
color: #eee !important;
border-color: #444 !important;
}
#app.dark-theme .jv-key {
color: #4a9eff !important;
}
#app.dark-theme .jv-number {
color: #f39c12 !important;
}
#app.dark-theme .jv-string {
color: #27ae60 !important;
}
#app.dark-theme .jv-boolean {
color: #e67e22 !important;
}
#app.dark-theme .jv-null {
color: #e74c3c !important;
}
#app.jv-container .jv-item.jv-object {
color: #32ba6d;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="show-file-page">
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
<div v-else>
<div v-if="parseResult.code">
<div class="file-meta-info-card">
<div class="file-meta-row">
<span class="file-meta-label">下载链接</span>
<a :href="downloadUrl" target="_blank" class="file-meta-link">{{ downloadUrl }}</a>
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件名</span>{{ fileTypeUtils.extractFileNameAndExt(downloadUrl).name }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">文件类型</span>{{ fileTypeUtils.getFileTypeClass({ fileName: fileTypeUtils.extractFileNameAndExt(downloadUrl).name }) }}
</div>
<div class="file-meta-row" v-if="parseResult.data?.sizeStr">
<span class="file-meta-label">文件大小</span>{{ parseResult.data.sizeStr }}
</div>
<div class="file-meta-row">
<span class="file-meta-label">在线预览</span>
<a :href="previewBaseUrl + encodeURIComponent(downloadUrl)" target="_blank" class="preview-btn">点击在线预览</a>
</div>
</div>
</div>
<div v-else style="text-align:center;margin-top:40px;">未获取到有效解析结果</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { previewBaseUrl } from '@/views/Home.vue'
export default {
name: 'ShowFile',
data() {
return {
loading: true,
error: '',
parseResult: {},
downloadUrl: '',
fileTypeUtils,
previewBaseUrl
}
},
methods: {
async fetchFile() {
const url = this.$route.query.url
if (!url) {
this.error = '缺少 url 参数'
this.loading = false
return
}
try {
const res = await axios.get('/json/parser', { params: { url } })
this.parseResult = res.data
this.downloadUrl = res.data.data?.directLink
} catch (e) {
this.error = '解析失败'
} finally {
this.loading = false
}
}
},
mounted() {
this.fetchFile()
}
}
</script>
<style scoped>
.show-file-page {
max-width: 600px;
margin: 40px auto;
}
.file-meta-info-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 18px 24px 12px 24px;
font-size: 1.02rem;
color: #333;
}
.file-meta-row {
display: flex;
align-items: center;
margin-bottom: 10px;
font-size: 1.01em;
}
.file-meta-label {
min-width: 90px;
color: #888;
font-weight: 500;
margin-right: 8px;
}
.file-meta-link {
color: #409eff;
word-break: break-all;
text-decoration: underline;
}
.preview-btn {
display: inline-block;
padding: 4px 16px;
background: #409eff;
color: #fff;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
margin-left: 8px;
transition: background 0.2s;
}
.preview-btn:hover {
background: #1867c0;
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="show-list-page">
<div class="list-title-wrap">
<h2 class="list-title">{{ url }} 目录</h2>
<div class="list-subtitle">
<a :href="url" target="_blank">原始分享链接</a>
</div>
</div>
<div style="text-align:right;margin-bottom:12px;">
<DarkMode @theme-change="toggleTheme" style="float: left;"/>
<el-radio-group v-model="viewMode" size="small" style="margin-left:20px;">
<el-radio-button label="pane">窗格</el-radio-button>
<el-radio-button label="tree">目录树</el-radio-button>
</el-radio-group>
</div>
<div v-if="loading" style="text-align:center;margin-top:40px;">加载中...</div>
<div v-else-if="error" style="color:red;text-align:center;margin-top:40px;">{{ error }}</div>
<div v-else>
<DirectoryTree
:file-list="directoryData"
:share-url="url"
:password="''"
:view-mode="viewMode"
/>
</div>
</div>
</template>
<script>
import axios from 'axios'
import DirectoryTree from '@/components/DirectoryTree'
import DarkMode from '@/components/DarkMode'
export default {
name: 'ShowList',
components: { DirectoryTree, DarkMode },
data() {
return {
loading: true,
error: '',
directoryData: [],
url: '',
viewMode: 'pane'
}
},
methods: {
async fetchList() {
this.url = this.$route.query.url
if (!this.url) {
this.error = '缺少 url 参数'
this.loading = false
return
}
try {
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
this.directoryData = res.data.data || []
} catch (e) {
this.error = '目录解析失败'
} finally {
this.loading = false
}
},
toggleTheme(isDark) {
if (isDark) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
}
},
mounted() {
this.fetchList()
}
}
</script>
<style scoped>
.show-list-page {
max-width: 900px;
margin: 40px auto;
}
.list-title-wrap {
text-align: center;
margin-bottom: 18px;
}
.list-title {
font-size: 2rem;
font-weight: bold;
color: #409eff;
margin-bottom: 4px;
word-break: break-all;
}
.list-subtitle {
font-size: 1.05rem;
color: #888;
margin-bottom: 2px;
}
.list-subtitle a {
color: #409eff;
text-decoration: underline;
}
.list-subtitle a:hover {
color: #1867c0;
}
</style>

View File

@@ -16,15 +16,27 @@ module.exports = {
host: '127.0.0.1',
port: 6444,
proxy: {
'/': {
target: 'http://127.0.0.1:6400', // 请求本地
'/parser': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/v2': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/json': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
'/d': {
target: 'http://127.0.0.1:6400/', // 请求本地
ws: false
},
}
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
// it can be accessed in list.html to inject the correct title.
name: 'Netdisk fast download',
resolve: {
alias: {

View File

@@ -59,7 +59,7 @@ public class URLParamUtil {
boolean firstParam = !decodedUrl.contains("?");
for (String paramName : params.names()) {
if (!paramName.equals("url") && !paramName.equals("pwd")) { // 忽略 "url" 和 "pwd" 参数
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId") && !paramName.equals("uuid")) { // 忽略 "url" 和 "pwd" 参数
if (firstParam) {
urlBuilder.append("?");
firstParam = false;

View File

@@ -5,6 +5,7 @@ import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.cache.CacheManager;
import cn.qaiu.lz.common.util.URLParamUtil;
import cn.qaiu.lz.web.model.CacheLinkInfo;
import cn.qaiu.lz.web.model.LinkInfoResp;
import cn.qaiu.lz.web.model.StatisticsInfo;
import cn.qaiu.lz.web.model.SysUser;
@@ -15,6 +16,7 @@ import cn.qaiu.parser.ParserCreate;
import cn.qaiu.vx.core.annotaions.RouteHandler;
import cn.qaiu.vx.core.annotaions.RouteMapping;
import cn.qaiu.vx.core.enums.RouteMethod;
import cn.qaiu.vx.core.model.JsonResult;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ResponseUtil;
import cn.qaiu.vx.core.util.SharedDataUtil;
@@ -26,6 +28,8 @@ import io.vertx.core.json.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
@@ -33,14 +37,8 @@ import java.util.stream.Collectors;
@Slf4j
public class ParserApi {
private final UserService userService = AsyncServiceUtil.getAsyncServiceInstance(UserService.class);
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
@RouteMapping(value = "/login", method = RouteMethod.GET)
public Future<SysUser> login(SysUser user) {
log.info("<------- login: {}", user.getUsername());
return userService.login(user);
}
@RouteMapping(value = "/statisticsInfo", method = RouteMethod.GET, order = 99)
public Future<StatisticsInfo> statisticsInfo() {
@@ -100,11 +98,17 @@ public class ParserApi {
}
@RouteMapping("/getFileList")
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd) {
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
String url = URLParamUtil.parserParams(request);
ParserCreate parserCreate = ParserCreate.fromShareUrl(url).setShareLinkInfoPwd(pwd);
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
if (StringUtils.isNotBlank(dirId)) {
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
}
if (StringUtils.isNotBlank(uuid)) {
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
}
return parserCreate.createTool().parseFileList();
}
@@ -130,7 +134,6 @@ public class ParserApi {
return parserCreate.createTool().parseById();
}
@RouteMapping("/redirectUrl/:type/:param")
public Future<Void> redirectUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
@@ -140,4 +143,51 @@ public class ParserApi {
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
/**
* 预览媒体文件
*/
@RouteMapping(value = "/view/:type/:key", method = RouteMethod.GET, order = 2)
public void view(HttpServerRequest request, HttpServerResponse response, String type, String key) {
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
new ServerApi().parseKeyJson(request, type, key).onSuccess(res -> {
redirect(response, previewURL, res);
}).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
});
}
private static void redirect(HttpServerResponse response, String previewURL, CacheLinkInfo res) {
String directLink = res.getDirectLink();
ResponseUtil.redirect(response, previewURL + URLEncoder.encode(directLink, StandardCharsets.UTF_8));
}
/**
* 预览媒体文件
*/
@RouteMapping(value = "/preview", method = RouteMethod.GET, order = 9)
public void viewURL(HttpServerRequest request, HttpServerResponse response, String pwd) {
String previewURL = SharedDataUtil.getJsonStringForServerConfig("previewURL");
new ServerApi().parseJson(request, pwd).onSuccess(res -> {
redirect(response, previewURL, res);
}).onFailure(e -> {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.toString()));
});
}
@RouteMapping("/viewUrl/:type/:param")
public Future<Void> viewUrl(HttpServerResponse response, String type, String param) {
Promise<Void> promise = Promise.promise();
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
getFileDownUrl(type, param)
.onSuccess(res -> {
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
ResponseUtil.redirect(response, url);
})
.onFailure(t -> promise.fail(t.fillInStackTrace()));
return promise.future();
}
}

View File

@@ -6,6 +6,8 @@ server:
enableDatabase: true
# 服务域名或者IP 生成二维码链接时需要
domainName: http://127.0.0.1:6401
# 预览服务URL
previewURL: https://nfd-parser.github.io/nfd-preview/preview.html?src=
# domainName: https://lz.qaiu.top
# 反向代理服务器配置路径(不用加后缀)
@@ -38,7 +40,7 @@ rateLimit:
# 是否启用限流
enable: true
# 限流的请求数
limit: 5
limit: 10
# 限流的时间窗口(单位秒)
timeWindow: 10
# 路径匹配规则

View File

@@ -0,0 +1,90 @@
# 网盘分享链接云解析服务 API 测试
# 本文件包含了系统所有API接口的测试请求
# 使用方法:
# 1. 先运行登录接口获取token
# 2. 将返回的token替换所有请求中的YOUR_TOKEN_HERE
# 3. 对于需要ID的请求将实际ID替换TOKEN_ID
### 用户接口 ###
### 登录接口
POST http://localhost:6400/api/user/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
### 用户注册
POST http://localhost:6400/api/user/register
Content-Type: application/json
{
"username": "testuser",
"password": "password123",
"email": "testuser@example.com",
"phone": "13800138000"
}
### 获取用户信息
# 使用登录接口返回的token替换下面的YOUR_TOKEN_HERE
GET http://localhost:6400/api/user/info
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
### 验证Token
POST http://localhost:6400/api/user/validate-token
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc"
}
### 更新用户信息
PUT http://localhost:6400/api/user/update
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
{
"email": "new-email@example.com",
"phone": "13900139000",
"avatar": "https://example.com/avatar.jpg"
}
### 管理员接口 ###
### 获取所有网盘Token
GET http://localhost:6400/api/admin/tokens
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjhhN2E3ZDc1LWUxNDEtNDFiOS05ODFhLWJmZGNjNzU2NjQyZCIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3NTA4MjUxMDMxOTEsImlhdCI6MTc1MDczODcwMzE5MSwiaXNzIjoibmV0ZGlzay1mYXN0LWRvd25sb2FkIn0.z4Dhwji1_yHEVx0sb3DN1n6HjlRmG8-Qr0Th5XIVeHc
### 添加网盘Token
POST http://localhost:6400/api/admin/token
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"type": "yidong",
"description": "移动云盘token",
"token": "abc123xyz456"
}
### 获取单个网盘Token
# 替换下面的TOKEN_ID为实际的token ID
GET http://localhost:6400/api/admin/token/TOKEN_ID
Authorization: Bearer YOUR_TOKEN_HERE
### 更新网盘Token
# 替换下面的TOKEN_ID为实际的token ID
PUT http://localhost:6400/api/admin/token/TOKEN_ID
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"description": "更新后的描述",
"token": "new-token-value"
}
### 删除网盘Token
# 替换下面的TOKEN_ID为实际的token ID
DELETE http://localhost:6400/api/admin/token/TOKEN_ID
Authorization: Bearer YOUR_TOKEN_HERE

View File

@@ -0,0 +1,25 @@
POST https://login.123pan.com/api/user/sign_in
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
App-Version: 3
Connection: keep-alive
Content-Type: application/json
LoginUuid: 694eff443c1896851f0fa32abbb8c6ec69a422aa21721f4556d1e9f07a568bee
Referer: https://login.123pan.com/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0
platform: web
sec-ch-ua: "Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
{
"passport": "",
"password": "",
"remember": true
}
###
POST http://

View File

@@ -135,3 +135,23 @@ POST https://www.123684.com/b/api/file/batch_download_share_info?3697171543=1749
Content-Type: application/json;charset=UTF-8
{"ShareKey":"LH3rTd-pENed","fileIdList":[{"fileId":17525951}]}
###
https://www.123865.com/b/api/share/get?limit=100&next=-1&orderBy=file_name&orderDirection=asc&shareKey=iaKtVv-FTaCd&SharePwd=qaiu&ParentFileId=0&Page=1&event=homeListFile&operateType=1&OrderId=
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
App-Version: 3
Connection: keep-alive
DNT: 1
Host: www.123865.com
Referer: https://www.123865.com/s/iaKtVv-FTaCd?%E6%8F%90%E5%8F%96%E7%A0%81%3Aqaiu
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
platform: web
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"