feat:版本迭代
28
.env.example
@@ -1,24 +1,8 @@
|
|||||||
# # 数据库配置
|
# jwt密钥 用于生成token加密
|
||||||
# DB_HOST=localhost
|
JWT_SECRET=""
|
||||||
# DB_USER=your_username
|
|
||||||
# DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
# # API密钥
|
# 用户注册码
|
||||||
# API_KEY=your_api_key
|
REGISTER_CODE='9527'
|
||||||
|
|
||||||
# # 其他敏感信息
|
# 服务端口
|
||||||
# CLOUD115_TOKEN=your_token
|
PORT=8009
|
||||||
|
|
||||||
# 代理信息
|
|
||||||
HTTP_PROXY_HOST=127.0.0.1
|
|
||||||
HTTP_PROXY_PORT=7890
|
|
||||||
|
|
||||||
# 115网盘配置
|
|
||||||
CLOUD115_USER_ID=your_user_id # 用户ID 可以不填
|
|
||||||
CLOUD115_COOKIE=your_cookie
|
|
||||||
|
|
||||||
# 夸克网盘配置
|
|
||||||
QUARK_COOKIE='your_cookie'
|
|
||||||
|
|
||||||
# RSS配置
|
|
||||||
RSS_BASE_URL=https://rsshub.rssforever.com/telegram/channel
|
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -4,6 +4,11 @@ dist/
|
|||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
|
*.tar
|
||||||
|
|
||||||
|
# 数据库数据
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
# 保留模板
|
# 保留模板
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
|||||||
45
Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 构建前端项目
|
||||||
|
FROM node:18-alpine as frontend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 构建后端项目
|
||||||
|
FROM node:18-alpine as backend-build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN rm -f database.sqlite
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 生产环境镜像
|
||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
# 安装 Nginx
|
||||||
|
RUN apk add --no-cache nginx
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制前端构建产物到 Nginx
|
||||||
|
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制 Nginx 配置文件
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# 复制后端构建产物到生产环境镜像
|
||||||
|
COPY --from=backend-build /app /app
|
||||||
|
|
||||||
|
# 安装生产环境依赖
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8008
|
||||||
|
|
||||||
|
# 启动 Nginx 和后端服务
|
||||||
|
CMD ["sh", "-c", "nginx -g 'daemon off;' & npm run start && wait"]
|
||||||
55
README.md
@@ -1,6 +1,7 @@
|
|||||||
# CloudSaver
|
# CloudSaver
|
||||||
|
|
||||||
一个基于 Vue 3 + Express 的网盘资源搜索与转存工具。
|
一个基于 Vue 3 + Express 的网盘资源搜索与转存工具。
|
||||||
|
可通过docker 一键部署。
|
||||||
|
|
||||||
## 特别声明
|
## 特别声明
|
||||||
|
|
||||||
@@ -12,32 +13,48 @@
|
|||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. 此项目的资源搜索需要用到代理环境,请自行搭建。
|
1. 此项目的资源搜索需要用到代理环境,请自行搭建。
|
||||||
2. 默认的代理地址为:`http://127.0.0.1`,端口为:`7890`,如需修改,请自行 `.env`中配置。
|
2. 新用户注册,管理员默认注册码:230713;普通用户默认注册码:9527
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- 支持多个资源订阅(电报群)源搜索
|
- 支持多个资源订阅(电报群)源搜索
|
||||||
- 支持 115 网盘与夸克网盘资源转存
|
- 支持 115 网盘与夸克网盘**一键**资源转存
|
||||||
- 支持关键词搜索与资源链接解析
|
- 支持关键词搜索与资源链接解析
|
||||||
- 支持转存文件夹展示与选择
|
- 支持转存文件夹展示与选择
|
||||||
|
- 支持多用户使用
|
||||||
|
- 支持豆瓣热门榜单
|
||||||
|
- 支持热门榜单资源搜索
|
||||||
|
|
||||||
## 预览
|
## 预览
|
||||||
|
|
||||||
|
### 登录/注册
|
||||||
|
|
||||||
|
<img src="./docs/images/login.png" width="400">
|
||||||
|
<img src="./docs/images/register.png" width="400">
|
||||||
|
|
||||||
### 最新资源搜索
|
### 最新资源搜索
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172442.png" width="400">
|
<img src="./docs/images/search.png" width="400">
|
||||||
|
|
||||||
### 转存
|
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172609.png" width="400">
|
|
||||||
|
|
||||||
### 关键词搜索
|
### 关键词搜索
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-172710.png" width="400">
|
<img src="./docs/images/search-movie.png" width="400">
|
||||||
|
|
||||||
|
### 热门榜单
|
||||||
|
|
||||||
|
<img src="./docs/images/hotmovie.png" width="400">
|
||||||
|
|
||||||
|
|
||||||
|
### 转存
|
||||||
|
|
||||||
|
<img src="./docs/images/save.png" width="400">
|
||||||
|
|
||||||
|
|
||||||
|
### 系统设置
|
||||||
|
|
||||||
|
<img src="./docs/images/setting.png" width="400">
|
||||||
|
|
||||||
### 直接链接解析
|
|
||||||
|
|
||||||
<img src="./docs/images/screenshot-20241216-173136.png" width="400">
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -55,6 +72,7 @@
|
|||||||
- Node.js
|
- Node.js
|
||||||
- Express
|
- Express
|
||||||
- TypeScript
|
- TypeScript
|
||||||
|
- Sqlite3
|
||||||
- RSS Parser
|
- RSS Parser
|
||||||
|
|
||||||
## 环境配置
|
## 环境配置
|
||||||
@@ -63,6 +81,8 @@
|
|||||||
|
|
||||||
本项目需要 `Node.js` 版本 `18.x` 或更高版本。请确保在运行项目之前安装了正确的版本。
|
本项目需要 `Node.js` 版本 `18.x` 或更高版本。请确保在运行项目之前安装了正确的版本。
|
||||||
|
|
||||||
|
推荐使用pnpm进行依赖包安装,默认支持workspace,可以避免版本冲突。
|
||||||
|
|
||||||
### 后端配置项
|
### 后端配置项
|
||||||
|
|
||||||
复制环境变量模板文件:
|
复制环境变量模板文件:
|
||||||
@@ -72,8 +92,6 @@ cp .env.example ./backend/.env
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 根据 `.env.example`文件在 `backend`目录下创建 `.env`文件。
|
- 根据 `.env.example`文件在 `backend`目录下创建 `.env`文件。
|
||||||
- `CLOUD115_COOKIE`:115 用户 cookie
|
|
||||||
- `QUARK_COOKIE`:quark cookie
|
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
@@ -89,8 +107,21 @@ cp .env.example ./backend/.env
|
|||||||
- 后端服务:先进入后端目录`backend`下`npm run build`构建,然后执行 `npm run start`启动,默认端口为`8009`。
|
- 后端服务:先进入后端目录`backend`下`npm run build`构建,然后执行 `npm run start`启动,默认端口为`8009`。
|
||||||
- 通过`nginx`配置代理服务,将前端api的请求映射到后端服务。
|
- 通过`nginx`配置代理服务,将前端api的请求映射到后端服务。
|
||||||
|
|
||||||
|
## Docker 部署
|
||||||
|
|
||||||
|
- 构建镜像:
|
||||||
|
```bash
|
||||||
|
# 构建示例
|
||||||
|
docker build --platform linux/amd64 -t cloud-saver . --no-cache
|
||||||
|
```
|
||||||
|
- 运行容器:
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8008:8008 --name cloud-saver cloud-saver
|
||||||
|
```
|
||||||
|
|
||||||
## 交流沟通
|
## 交流沟通
|
||||||
|
|
||||||
|
<img src="./docs/images/20250220115710.jpg" width="400">
|
||||||
<img src="./docs/images/20241217122628.jpg" width="400">
|
<img src="./docs/images/20241217122628.jpg" width="400">
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -9,19 +9,25 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.18.3",
|
"express": "^4.18.3",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/node": "^20.11.25",
|
"@types/node": "^20.11.25",
|
||||||
"@types/tunnel": "^0.0.7",
|
"@types/tunnel": "^0.0.7",
|
||||||
"nodemon": "^3.1.0",
|
"nodemon": "^3.1.0",
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
|
||||||
|
import "./types/express";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import routes from "./routes/api";
|
import routes from "./routes/api";
|
||||||
import { errorHandler } from "./middleware/errorHandler";
|
import { errorHandler } from "./middleware/errorHandler";
|
||||||
|
import sequelize from "./config/database";
|
||||||
|
import { authMiddleware } from "./middleware/auth";
|
||||||
|
import GlobalSetting from "./models/GlobalSetting";
|
||||||
|
import Searcher from "./services/Searcher";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -17,14 +23,54 @@ app.use(
|
|||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// 应用 token 验证中间件,排除登录和注册接口
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (
|
||||||
|
req.path === "/user/login" ||
|
||||||
|
req.path === "/user/register" ||
|
||||||
|
req.path.includes("tele-images")
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
authMiddleware(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
app.use("/", routes);
|
app.use("/", routes);
|
||||||
|
|
||||||
|
const initializeGlobalSettings = async () => {
|
||||||
|
const settings = await GlobalSetting.findOne();
|
||||||
|
if (!settings) {
|
||||||
|
await GlobalSetting.create({
|
||||||
|
httpProxyHost: "127.0.0.1",
|
||||||
|
httpProxyPort: 7890,
|
||||||
|
isProxyEnabled: true,
|
||||||
|
CommonUserCode: 9527,
|
||||||
|
AdminUserCode: 230713,
|
||||||
|
});
|
||||||
|
console.log("Global settings initialized with default values.");
|
||||||
|
}
|
||||||
|
await Searcher.updateAxiosInstance();
|
||||||
|
};
|
||||||
|
|
||||||
// 错误处理
|
// 错误处理
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8009;
|
const PORT = process.env.PORT || 8009;
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on port ${PORT}`);
|
// 在同步前禁用外键约束,同步后重新启用
|
||||||
});
|
sequelize
|
||||||
|
.query("PRAGMA foreign_keys = OFF") // 禁用外键
|
||||||
|
.then(() => sequelize.sync({ alter: true }))
|
||||||
|
.then(() => sequelize.query("PRAGMA foreign_keys = ON")) // 重新启用外键
|
||||||
|
.then(() => {
|
||||||
|
app.listen(PORT, async () => {
|
||||||
|
await initializeGlobalSettings();
|
||||||
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Database sync failed:", error);
|
||||||
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
9
backend/src/config/database.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// backend/src/config/database.ts
|
||||||
|
import { Sequelize } from "sequelize";
|
||||||
|
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: "sqlite",
|
||||||
|
storage: "./data/database.sqlite",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default sequelize;
|
||||||
@@ -30,6 +30,8 @@ interface HttpProxyConfig {
|
|||||||
port: string;
|
port: string;
|
||||||
}
|
}
|
||||||
interface Config {
|
interface Config {
|
||||||
|
jwtSecret: string;
|
||||||
|
registerCode: string;
|
||||||
rss: {
|
rss: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
channels: Channel[];
|
channels: Channel[];
|
||||||
@@ -44,6 +46,7 @@ interface Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
|
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
|
||||||
rss: {
|
rss: {
|
||||||
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
|
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
|
||||||
channels: [
|
channels: [
|
||||||
@@ -61,14 +64,15 @@ export const config: Config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
registerCode: process.env.REGISTER_CODE || "9527",
|
||||||
|
|
||||||
telegram: {
|
telegram: {
|
||||||
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
||||||
},
|
},
|
||||||
|
|
||||||
httpProxy: {
|
httpProxy: {
|
||||||
host: process.env.HTTP_PROXY_HOST || "127.0.0.1",
|
host: process.env.HTTP_PROXY_HOST || "",
|
||||||
port: process.env.HTTP_PROXY_PORT || "7890",
|
port: process.env.HTTP_PROXY_PORT || "",
|
||||||
},
|
},
|
||||||
|
|
||||||
cloudPatterns: {
|
cloudPatterns: {
|
||||||
@@ -77,7 +81,7 @@ export const config: Config = {
|
|||||||
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g,
|
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g,
|
||||||
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g,
|
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g,
|
||||||
// pan115有两个域名 115.com 和 anxia.com
|
// pan115有两个域名 115.com 和 anxia.com
|
||||||
pan115: /https?:\/\/(?:115|anxia)\.com\/s\/[^\s<>"]+/g,
|
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
|
||||||
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,61 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { Cloud115Service } from "../services/Cloud115Service";
|
import { Cloud115Service } from "../services/Cloud115Service";
|
||||||
import { config } from "../config";
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
import handleError from "../utils/handleError";
|
import UserSetting from "../models/UserSetting";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
|
||||||
|
|
||||||
const { cookie } = config.cloud115;
|
const cloud115 = new Cloud115Service();
|
||||||
|
const setCookie = async (req: Request) => {
|
||||||
const cloud115 = new Cloud115Service(cookie);
|
const userId = req.user?.userId;
|
||||||
|
const userSetting = await UserSetting.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
console.log(userSetting?.dataValues.cloud115Cookie);
|
||||||
|
if (userSetting && userSetting.dataValues.cloud115Cookie) {
|
||||||
|
cloud115.setCookie(userSetting.dataValues.cloud115Cookie);
|
||||||
|
} else {
|
||||||
|
throw new Error("请先设置115网盘cookie");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const cloud115Controller = {
|
export const cloud115Controller = {
|
||||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { shareCode, receiveCode } = req.query;
|
const { shareCode, receiveCode } = req.query;
|
||||||
|
await setCookie(req);
|
||||||
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
|
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取分享信息失败", next);
|
console.log(error);
|
||||||
|
sendError(res, { message: (error as Error).message || "获取分享信息失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { parentCid } = req.query;
|
const { parentCid } = req.query;
|
||||||
|
await setCookie(req);
|
||||||
const result = await cloud115.getFolderList(parentCid as string);
|
const result = await cloud115.getFolderList(parentCid as string);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取目录列表失败", next);
|
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
async saveFile(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { shareCode, receiveCode, fileId, folderId } = req.body;
|
const { shareCode, receiveCode, fileId, folderId } = req.body;
|
||||||
|
await setCookie(req);
|
||||||
const result = await cloud115.saveSharedFile({
|
const result = await cloud115.saveSharedFile({
|
||||||
shareCode,
|
shareCode,
|
||||||
receiveCode,
|
receiveCode,
|
||||||
fileId,
|
fileId,
|
||||||
cid: folderId,
|
cid: folderId,
|
||||||
});
|
});
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "保存文件失败", next);
|
sendError(res, { message: (error as Error).message || "保存文件失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Cloud115ServiceInstance = cloud115;
|
||||||
|
|||||||
22
backend/src/controllers/douban.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import DoubanService from "../services/DoubanService";
|
||||||
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
|
|
||||||
|
const doubanService = new DoubanService();
|
||||||
|
|
||||||
|
export const doubanController = {
|
||||||
|
async getDoubanHotList(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
|
||||||
|
const result = await doubanService.getHotList({
|
||||||
|
type: type as string,
|
||||||
|
tag: tag as string,
|
||||||
|
page_limit: page_limit as string,
|
||||||
|
page_start: page_start as string,
|
||||||
|
});
|
||||||
|
sendSuccess(res, result);
|
||||||
|
} catch (error) {
|
||||||
|
sendError(res, { message: "获取热门列表失败" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,40 +1,52 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { QuarkService } from "../services/QuarkService";
|
import { QuarkService } from "../services/QuarkService";
|
||||||
import { config } from "../config";
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
import UserSetting from "../models/UserSetting";
|
||||||
import handleError from "../utils/handleError";
|
|
||||||
|
|
||||||
const { cookie } = config.quark;
|
const quark = new QuarkService();
|
||||||
|
|
||||||
const quark = new QuarkService(cookie);
|
const setCookie = async (req: Request) => {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const userSetting = await UserSetting.findOne({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
if (userSetting && userSetting.dataValues.quarkCookie) {
|
||||||
|
quark.setCookie(userSetting.dataValues.quarkCookie);
|
||||||
|
} else {
|
||||||
|
throw new Error("请先设置夸克网盘cookie");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const quarkController = {
|
export const quarkController = {
|
||||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { pwdId, passcode } = req.query;
|
const { pwdId, passcode } = req.query;
|
||||||
|
await setCookie(req);
|
||||||
const result = await quark.getShareInfo(pwdId as string, passcode as string);
|
const result = await quark.getShareInfo(pwdId as string, passcode as string);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取分享信息失败", next);
|
sendError(res, { message: "获取分享信息失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { parentCid } = req.query;
|
const { parentCid } = req.query;
|
||||||
|
await setCookie(req);
|
||||||
const result = await quark.getFolderList(parentCid as string);
|
const result = await quark.getFolderList(parentCid as string);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取目录列表失败", next);
|
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
async saveFile(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
|
await setCookie(req);
|
||||||
const result = await quark.saveSharedFile(req.body);
|
const result = await quark.saveSharedFile(req.body);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "保存文件失败", next);
|
sendError(res, { message: (error as Error).message || "保存文件失败" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { RSSSearcher } from "../services/RSSSearcher";
|
import { RSSSearcher } from "../services/RSSSearcher";
|
||||||
import { Searcher } from "../services/Searcher";
|
import Searcher from "../services/Searcher";
|
||||||
import { handleResponse } from "../utils/responseHandler";
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
import handleError from "../utils/handleError";
|
|
||||||
|
|
||||||
export const resourceController = {
|
export const resourceController = {
|
||||||
async rssSearch(req: Request, res: Response, next: NextFunction) {
|
async rssSearch(req: Request, res: Response, next: NextFunction) {
|
||||||
@@ -10,23 +9,26 @@ export const resourceController = {
|
|||||||
const { keyword } = req.query;
|
const { keyword } = req.query;
|
||||||
const searcher = new RSSSearcher();
|
const searcher = new RSSSearcher();
|
||||||
const result = await searcher.searchAll(keyword as string);
|
const result = await searcher.searchAll(keyword as string);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取资源发生未知错误", next);
|
sendError(res, {
|
||||||
|
message: (error as Error).message || "RSS 搜索失败",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async search(req: Request, res: Response, next: NextFunction) {
|
async search(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
|
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
|
||||||
const searcher = new Searcher();
|
const result = await Searcher.searchAll(
|
||||||
const result = await searcher.searchAll(
|
|
||||||
keyword as string,
|
keyword as string,
|
||||||
channelId as string,
|
channelId as string,
|
||||||
lastMessageId as string
|
lastMessageId as string
|
||||||
);
|
);
|
||||||
handleResponse(res, result, true);
|
sendSuccess(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(res, error, "获取资源发生未知错误", next);
|
sendError(res, {
|
||||||
|
message: (error as Error).message || "搜索资源失败",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
57
backend/src/controllers/setting.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
|
import Searcher from "../services/Searcher";
|
||||||
|
import UserSetting from "../models/UserSetting";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
|
||||||
|
export const settingController = {
|
||||||
|
async get(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const role = req.user?.role;
|
||||||
|
if (userId !== null) {
|
||||||
|
let userSettings = await UserSetting.findOne({ where: { userId } });
|
||||||
|
if (!userSettings) {
|
||||||
|
userSettings = {
|
||||||
|
userId: userId,
|
||||||
|
cloud115Cookie: "",
|
||||||
|
quarkCookie: "",
|
||||||
|
} as UserSetting;
|
||||||
|
if (userSettings) {
|
||||||
|
await UserSetting.create(userSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const globalSetting = await GlobalSetting.findOne();
|
||||||
|
sendSuccess(res, {
|
||||||
|
data: {
|
||||||
|
userSettings,
|
||||||
|
globalSetting: role === 1 ? globalSetting : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendError(res, { message: "用户ID无效" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("获取设置失败:" + error);
|
||||||
|
sendError(res, { message: (error as Error).message || "获取设置失败" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async save(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.userId;
|
||||||
|
const role = req.user?.role;
|
||||||
|
if (userId !== null) {
|
||||||
|
const { userSettings, globalSetting } = req.body;
|
||||||
|
await UserSetting.update(userSettings, { where: { userId } });
|
||||||
|
if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} });
|
||||||
|
Searcher.updateAxiosInstance();
|
||||||
|
sendSuccess(res, {
|
||||||
|
message: "保存成功",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("保存设置失败:" + error);
|
||||||
|
sendError(res, { message: (error as Error).message || "保存设置失败" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
57
backend/src/controllers/teleImages.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import axios, { AxiosInstance } from "axios";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import tunnel from "tunnel";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||||
|
|
||||||
|
export class ImageControll {
|
||||||
|
private axiosInstance: AxiosInstance | null = null;
|
||||||
|
private isUpdate = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeAxiosInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeAxiosInstance(isUpdate = false) {
|
||||||
|
let settings = null;
|
||||||
|
if (isUpdate) {
|
||||||
|
settings = await GlobalSetting.findOne();
|
||||||
|
this.isUpdate = isUpdate;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||||
|
this.axiosInstance = axios.create({
|
||||||
|
timeout: 3000,
|
||||||
|
httpsAgent: tunnel.httpsOverHttp({
|
||||||
|
proxy: {
|
||||||
|
host: globalSetting.httpProxyHost,
|
||||||
|
port: globalSetting.httpProxyPort,
|
||||||
|
headers: {
|
||||||
|
"Proxy-Authorization": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getImages(req: Request, res: Response, url: string) {
|
||||||
|
try {
|
||||||
|
if (!this.isUpdate) await this.initializeAxiosInstance(true);
|
||||||
|
const response = await this.axiosInstance?.get(url, { responseType: "stream" });
|
||||||
|
res.set("Content-Type", response?.headers["content-type"]);
|
||||||
|
response?.data.pipe(res);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send("Image fetch error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const iamgesInstance = new ImageControll();
|
||||||
|
|
||||||
|
export const imageControll = {
|
||||||
|
getImages: async (req: Request, res: Response) => {
|
||||||
|
const url = req.query.url as string;
|
||||||
|
iamgesInstance.getImages(req, res, url);
|
||||||
|
},
|
||||||
|
};
|
||||||
62
backend/src/controllers/user.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import User from "../models/User";
|
||||||
|
import { config } from "../config";
|
||||||
|
import { sendSuccess, sendError } from "../utils/response";
|
||||||
|
|
||||||
|
const isValidInput = (input: string): boolean => {
|
||||||
|
// 检查是否包含空格或汉字
|
||||||
|
const regex = /^[^\s\u4e00-\u9fa5]+$/;
|
||||||
|
return regex.test(input);
|
||||||
|
};
|
||||||
|
export const userController = {
|
||||||
|
async register(req: Request, res: Response) {
|
||||||
|
const { username, password, registerCode } = req.body;
|
||||||
|
const globalSetting = await GlobalSetting.findOne();
|
||||||
|
const registerCodeList = [
|
||||||
|
globalSetting?.dataValues.CommonUserCode,
|
||||||
|
globalSetting?.dataValues.AdminUserCode,
|
||||||
|
];
|
||||||
|
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
|
||||||
|
return sendError(res, { message: "注册码错误" });
|
||||||
|
}
|
||||||
|
// 验证输入
|
||||||
|
if (!isValidInput(username) || !isValidInput(password)) {
|
||||||
|
return sendError(res, { message: "用户名、密码或注册码不能包含空格或汉字" });
|
||||||
|
}
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingUser = await User.findOne({ where: { username } });
|
||||||
|
if (existingUser) {
|
||||||
|
return sendError(res, { message: "用户名已存在" });
|
||||||
|
}
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
try {
|
||||||
|
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
|
||||||
|
const user = await User.create({ username, password: hashedPassword, role });
|
||||||
|
sendSuccess(res, {
|
||||||
|
data: user,
|
||||||
|
message: "用户注册成功",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
sendError(res, { message: error.message || "用户注册失败" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(req: Request, res: Response) {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
const user = await User.findOne({ where: { username } });
|
||||||
|
if (!user || !(await bcrypt.compare(password, user.password))) {
|
||||||
|
return sendError(res, { message: "用户名或密码错误" });
|
||||||
|
}
|
||||||
|
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
|
||||||
|
expiresIn: "6h",
|
||||||
|
});
|
||||||
|
sendSuccess(res, {
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
43
backend/src/middleware/auth.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// filepath: /D:/code/CloudDiskDown/backend/src/middleware/auth.ts
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
import User from "../models/User";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
|
interface AuthenticatedRequest extends Request {
|
||||||
|
user?: {
|
||||||
|
userId: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authMiddleware = async (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (req.path === "/user/login" || req.path === "/user/register") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ message: "未提供 token" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
|
||||||
|
|
||||||
|
req.user = {
|
||||||
|
userId: decoded.userId,
|
||||||
|
role: decoded.role,
|
||||||
|
};
|
||||||
|
const user = await User.findOne({ where: { userId: decoded.userId } });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ message: "无效的 token" });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).json({ message: "无效的 token" });
|
||||||
|
}
|
||||||
|
};
|
||||||
67
backend/src/models/GlobalSetting.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
|
||||||
|
export interface GlobalSettingAttributes {
|
||||||
|
id: number;
|
||||||
|
httpProxyHost: string;
|
||||||
|
httpProxyPort: number;
|
||||||
|
isProxyEnabled: boolean;
|
||||||
|
CommonUserCode: number;
|
||||||
|
AdminUserCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalSettingCreationAttributes extends Optional<GlobalSettingAttributes, "id"> {}
|
||||||
|
|
||||||
|
class GlobalSetting
|
||||||
|
extends Model<GlobalSettingAttributes, GlobalSettingCreationAttributes>
|
||||||
|
implements GlobalSettingAttributes
|
||||||
|
{
|
||||||
|
public id!: number;
|
||||||
|
public httpProxyHost!: string;
|
||||||
|
public httpProxyPort!: number;
|
||||||
|
public isProxyEnabled!: boolean;
|
||||||
|
public CommonUserCode!: number;
|
||||||
|
public AdminUserCode!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalSetting.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
httpProxyHost: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "127.0.0.1",
|
||||||
|
},
|
||||||
|
httpProxyPort: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 7890,
|
||||||
|
},
|
||||||
|
isProxyEnabled: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
CommonUserCode: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 9527,
|
||||||
|
},
|
||||||
|
AdminUserCode: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 230713,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "GlobalSetting",
|
||||||
|
tableName: "global_settings",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GlobalSetting;
|
||||||
62
backend/src/models/User.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
|
||||||
|
interface UserAttributes {
|
||||||
|
id: number;
|
||||||
|
userId?: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
role: number; // 修改为数字类型
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCreationAttributes extends Optional<UserAttributes, "id"> {}
|
||||||
|
|
||||||
|
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||||
|
public id!: number;
|
||||||
|
public userId!: number;
|
||||||
|
public username!: string;
|
||||||
|
public password!: string;
|
||||||
|
public role!: number; // 实现数字类型的角色属性
|
||||||
|
}
|
||||||
|
|
||||||
|
User.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false, // 显式设置为不可为空
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID, // 对外暴露的不可预测ID
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
unique: true,
|
||||||
|
allowNull: false, // 显式设置为不可为空
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: DataTypes.INTEGER, // 修改为数字类型
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0, // 默认值为普通用户
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "User",
|
||||||
|
tableName: "users",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 角色映射
|
||||||
|
// 0: 普通用户
|
||||||
|
// 1: 管理员
|
||||||
|
|
||||||
|
export default User;
|
||||||
72
backend/src/models/UserSetting.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { DataTypes, Model, Optional } from "sequelize";
|
||||||
|
import sequelize from "../config/database";
|
||||||
|
import User from "./User";
|
||||||
|
|
||||||
|
interface UserSettingAttributes {
|
||||||
|
id: number;
|
||||||
|
userId: string;
|
||||||
|
cloud115UserId?: string;
|
||||||
|
cloud115Cookie: string;
|
||||||
|
quarkCookie: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSettingCreationAttributes extends Optional<UserSettingAttributes, "id"> {}
|
||||||
|
|
||||||
|
class UserSetting
|
||||||
|
extends Model<UserSettingAttributes, UserSettingCreationAttributes>
|
||||||
|
implements UserSettingAttributes
|
||||||
|
{
|
||||||
|
public id!: number;
|
||||||
|
public userId!: string;
|
||||||
|
public cloud115UserId?: string;
|
||||||
|
public cloud115Cookie!: string;
|
||||||
|
public quarkCookie!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSetting.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
references: {
|
||||||
|
model: User,
|
||||||
|
key: "userId",
|
||||||
|
},
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
},
|
||||||
|
cloud115UserId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
cloud115Cookie: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
quarkCookie: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "UserSetting",
|
||||||
|
tableName: "user_settings",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
User.hasOne(UserSetting, {
|
||||||
|
foreignKey: "userId",
|
||||||
|
as: "settings",
|
||||||
|
});
|
||||||
|
UserSetting.belongsTo(User, {
|
||||||
|
foreignKey: "userId",
|
||||||
|
as: "user",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserSetting;
|
||||||
@@ -2,9 +2,21 @@ import express from "express";
|
|||||||
import { cloud115Controller } from "../controllers/cloud115";
|
import { cloud115Controller } from "../controllers/cloud115";
|
||||||
import { quarkController } from "../controllers/quark";
|
import { quarkController } from "../controllers/quark";
|
||||||
import { resourceController } from "../controllers/resource";
|
import { resourceController } from "../controllers/resource";
|
||||||
|
import { doubanController } from "../controllers/douban";
|
||||||
|
import { imageControll } from "../controllers/teleImages";
|
||||||
|
import settingRoutes from "./setting";
|
||||||
|
import userRoutes from "./user";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// 用户相关路由
|
||||||
|
router.use("/user", userRoutes);
|
||||||
|
|
||||||
|
router.use("/tele-images", imageControll.getImages);
|
||||||
|
|
||||||
|
// 设置相关路由
|
||||||
|
router.use("/setting", settingRoutes);
|
||||||
|
|
||||||
// 资源搜索
|
// 资源搜索
|
||||||
router.get("/search", resourceController.search);
|
router.get("/search", resourceController.search);
|
||||||
router.get("/rssSearch", resourceController.rssSearch);
|
router.get("/rssSearch", resourceController.rssSearch);
|
||||||
@@ -19,4 +31,7 @@ router.get("/quark/share-info", quarkController.getShareInfo);
|
|||||||
router.get("/quark/folders", quarkController.getFolderList);
|
router.get("/quark/folders", quarkController.getFolderList);
|
||||||
router.post("/quark/save", quarkController.saveFile);
|
router.post("/quark/save", quarkController.saveFile);
|
||||||
|
|
||||||
|
// 获取豆瓣热门列表
|
||||||
|
router.get("/douban/hot", doubanController.getDoubanHotList);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
9
backend/src/routes/setting.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { settingController } from "../controllers/setting";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/get", settingController.get);
|
||||||
|
router.post("/save", settingController.save);
|
||||||
|
|
||||||
|
export default router;
|
||||||
10
backend/src/routes/user.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// backend/src/routes/user.ts
|
||||||
|
import express from "express";
|
||||||
|
import { userController } from "../controllers/user";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/register", userController.register);
|
||||||
|
router.post("/login", userController.login);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -6,12 +6,9 @@ import { ShareInfoResponse } from "../types/cloud115";
|
|||||||
|
|
||||||
export class Cloud115Service {
|
export class Cloud115Service {
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
|
private cookie: string = "";
|
||||||
|
|
||||||
constructor(cookie: string) {
|
constructor(cookie?: string) {
|
||||||
if (!cookie) {
|
|
||||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = createAxiosInstance(
|
this.api = createAxiosInstance(
|
||||||
"https://webapi.115.com",
|
"https://webapi.115.com",
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
@@ -29,91 +26,77 @@ export class Cloud115Service {
|
|||||||
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
|
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
cookie: cookie,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (cookie) {
|
||||||
|
this.setCookie(cookie);
|
||||||
|
} else {
|
||||||
|
console.log("请注意:115网盘需要提供cookie进行身份验证");
|
||||||
|
}
|
||||||
|
this.api.interceptors.request.use((config) => {
|
||||||
|
config.headers.cookie = cookie || this.cookie;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCookie(cookie: string) {
|
||||||
|
this.cookie = cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
||||||
try {
|
const response = await this.api.get("/share/snap", {
|
||||||
const response = await this.api.get("/share/snap", {
|
params: {
|
||||||
params: {
|
share_code: shareCode,
|
||||||
share_code: shareCode,
|
receive_code: receiveCode,
|
||||||
receive_code: receiveCode,
|
offset: 0,
|
||||||
offset: 0,
|
limit: 20,
|
||||||
limit: 20,
|
cid: "",
|
||||||
cid: "",
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data?.state && response.data.data?.list?.length > 0) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
list: response.data.data.list.map((item: any) => ({
|
|
||||||
fileId: item.cid,
|
|
||||||
fileName: item.n,
|
|
||||||
fileSize: item.s,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (response.data?.state && response.data.data?.list?.length > 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
data: response.data.data.list.map((item: any) => ({
|
||||||
error: "未找到文件信息",
|
fileId: item.cid,
|
||||||
};
|
fileName: item.n,
|
||||||
} catch (error) {
|
fileSize: item.s,
|
||||||
return {
|
})),
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
throw new Error("未找到文件信息");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFolderList(parentCid = "0") {
|
async getFolderList(parentCid = "0") {
|
||||||
try {
|
const response = await this.api.get("/files", {
|
||||||
const response = await this.api.get("/files", {
|
params: {
|
||||||
params: {
|
aid: 1,
|
||||||
aid: 1,
|
cid: parentCid,
|
||||||
cid: parentCid,
|
o: "user_ptime",
|
||||||
o: "user_ptime",
|
asc: 0,
|
||||||
asc: 0,
|
offset: 0,
|
||||||
offset: 0,
|
show_dir: 1,
|
||||||
show_dir: 1,
|
limit: 50,
|
||||||
limit: 50,
|
type: 0,
|
||||||
type: 0,
|
format: "json",
|
||||||
format: "json",
|
star: 0,
|
||||||
star: 0,
|
suffix: "",
|
||||||
suffix: "",
|
natsort: 1,
|
||||||
natsort: 1,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data?.state) {
|
if (response.data?.state) {
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: response.data.data
|
|
||||||
.filter((item: any) => item.cid)
|
|
||||||
.map((folder: any) => ({
|
|
||||||
cid: folder.cid,
|
|
||||||
name: folder.n,
|
|
||||||
path: response.data.path,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
Logger.error("获取目录列表失败:", response.data.error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "获取115pan目录列表失败:" + response.data.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
data: response.data.data
|
||||||
error: "获取115pan目录列表失败",
|
.filter((item: any) => item.cid && !!item.ns)
|
||||||
|
.map((folder: any) => ({
|
||||||
|
cid: folder.cid,
|
||||||
|
name: folder.n,
|
||||||
|
path: response.data.path,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
Logger.error("获取目录列表失败:", response.data.error);
|
||||||
|
throw new Error("获取115pan目录列表失败:" + response.data.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,26 +106,23 @@ export class Cloud115Service {
|
|||||||
receiveCode: string;
|
receiveCode: string;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
const param = new URLSearchParams({
|
||||||
const param = new URLSearchParams({
|
cid: params.cid,
|
||||||
cid: params.cid,
|
user_id: config.cloud115.userId,
|
||||||
user_id: config.cloud115.userId,
|
share_code: params.shareCode,
|
||||||
share_code: params.shareCode,
|
receive_code: params.receiveCode,
|
||||||
receive_code: params.receiveCode,
|
file_id: params.fileId,
|
||||||
file_id: params.fileId,
|
});
|
||||||
});
|
const response = await this.api.post("/share/receive", param.toString());
|
||||||
const response = await this.api.post("/share/receive", param.toString());
|
Logger.info("保存文件:", response.data);
|
||||||
|
if (response.data.state) {
|
||||||
return {
|
return {
|
||||||
success: response.data.state,
|
message: response.data.error,
|
||||||
error: response.data.error,
|
data: response.data.data,
|
||||||
data: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
Logger.error("保存文件失败:", response.data.error);
|
||||||
|
throw new Error("保存115pan文件失败:" + response.data.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
backend/src/services/DoubanService.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { AxiosHeaders, AxiosInstance } from "axios";
|
||||||
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
|
||||||
|
class DoubanService {
|
||||||
|
private baseUrl: string;
|
||||||
|
private api: AxiosInstance;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = "https://movie.douban.com/j";
|
||||||
|
this.api = createAxiosInstance(
|
||||||
|
this.baseUrl,
|
||||||
|
AxiosHeaders.from({
|
||||||
|
accept: "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||||
|
priority: "u=1, i",
|
||||||
|
"sec-ch-ua": '"Not A(Brand";v="8", "Chromium";v="132", "Microsoft Edge";v="132"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
|
"sec-fetch-dest": "empty",
|
||||||
|
"sec-fetch-mode": "cors",
|
||||||
|
"sec-fetch-site": "same-origin",
|
||||||
|
"x-requested-with": "XMLHttpRequest",
|
||||||
|
cookie:
|
||||||
|
'll="118282"; bid=StA6AQFsAWQ; _pk_id.100001.4cf6=6448be57b1b5ca7e.1723172321.; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmv=30149280.6282; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmz=30149280.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; __utmz=223695111.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; douban-fav-remind=1; __utmc=30149280; __utmc=223695111; _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1739176523%2C%22https%3A%2F%2Fsearch.douban.com%2Fmovie%2Fsubject_search%3Fsearch_text%3D%E8%84%91%E6%B4%9E%E5%A4%A7%E5%BC%80%26cat%3D1002%22%5D; _pk_ses.100001.4cf6=1; ap_v=0,6.0; __utma=30149280.859303574.1723448979.1739167503.1739176523.42; __utmb=30149280.0.10.1739176523; __utma=223695111.1882744177.1723448979.1739167503.1739176523.42; __utmb=223695111.0.10.1739176523',
|
||||||
|
Referer: "https://movie.douban.com/",
|
||||||
|
"Referrer-Policy": "unsafe-url",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHotList(params: { type: string; tag: string; page_limit: string; page_start: string }) {
|
||||||
|
try {
|
||||||
|
const response = await this.api.get("/search_subjects", {
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
|
if (response.data && response.data.subjects) {
|
||||||
|
return {
|
||||||
|
data: response.data.subjects,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching hot list:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DoubanService;
|
||||||
@@ -4,16 +4,13 @@ import { createAxiosInstance } from "../utils/axiosInstance";
|
|||||||
|
|
||||||
export class QuarkService {
|
export class QuarkService {
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
|
private cookie: string = "";
|
||||||
|
|
||||||
constructor(cookie: string) {
|
constructor(cookie?: string) {
|
||||||
if (!cookie) {
|
|
||||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api = createAxiosInstance(
|
this.api = createAxiosInstance(
|
||||||
"https://drive-h.quark.cn",
|
"https://drive-h.quark.cn",
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
cookie: cookie,
|
cookie: this.cookie,
|
||||||
accept: "application/json, text/plain, */*",
|
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",
|
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
@@ -26,125 +23,112 @@ export class QuarkService {
|
|||||||
"sec-fetch-site": "same-site",
|
"sec-fetch-site": "same-site",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (cookie) {
|
||||||
|
this.setCookie(cookie);
|
||||||
|
} else {
|
||||||
|
console.log("请注意:夸克网盘需要提供cookie进行身份验证");
|
||||||
|
}
|
||||||
|
this.api.interceptors.request.use((config) => {
|
||||||
|
config.headers.cookie = cookie || this.cookie;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCookie(cookie: string) {
|
||||||
|
this.cookie = cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareInfo(pwdId: string, passcode = "") {
|
async getShareInfo(pwdId: string, passcode = "") {
|
||||||
try {
|
const response = await this.api.post(
|
||||||
const response = await this.api.post(
|
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
|
||||||
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
|
{
|
||||||
{
|
pwd_id: pwdId,
|
||||||
pwd_id: pwdId,
|
passcode,
|
||||||
passcode: "",
|
}
|
||||||
}
|
);
|
||||||
);
|
if (response.data?.status === 200 && response.data.data) {
|
||||||
if (response.data?.status === 200 && response.data.data) {
|
const fileInfo = response.data.data;
|
||||||
const fileInfo = response.data.data;
|
if (fileInfo.stoken) {
|
||||||
if (fileInfo.stoken) {
|
let res = await this.getShareList(pwdId, fileInfo.stoken);
|
||||||
let res = await this.getShareList(pwdId, fileInfo.stoken);
|
return {
|
||||||
return {
|
data: res,
|
||||||
success: true,
|
};
|
||||||
data: res,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "未找到文件信息",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
throw new Error("获取夸克分享信息失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getShareList(pwdId: string, stoken: string) {
|
async getShareList(pwdId: string, stoken: string) {
|
||||||
try {
|
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
|
||||||
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
|
params: {
|
||||||
params: {
|
pr: "ucpro",
|
||||||
pr: "ucpro",
|
fr: "pc",
|
||||||
fr: "pc",
|
uc_param_str: "",
|
||||||
uc_param_str: "",
|
pwd_id: pwdId,
|
||||||
pwd_id: pwdId,
|
stoken: stoken,
|
||||||
stoken: stoken,
|
pdir_fid: "0",
|
||||||
pdir_fid: "0",
|
force: "0",
|
||||||
force: "0",
|
_page: "1",
|
||||||
_page: "1",
|
_size: "50",
|
||||||
_size: "50",
|
_fetch_banner: "1",
|
||||||
_fetch_banner: "1",
|
_fetch_share: "1",
|
||||||
_fetch_share: "1",
|
_fetch_total: "1",
|
||||||
_fetch_total: "1",
|
_sort: "file_type:asc,updated_at:desc",
|
||||||
_sort: "file_type:asc,updated_at:desc",
|
__dt: "1589",
|
||||||
__dt: "1589",
|
__t: Date.now(),
|
||||||
__t: Date.now(),
|
},
|
||||||
},
|
});
|
||||||
});
|
if (response.data?.data) {
|
||||||
if (response.data?.data) {
|
const list = response.data.data.list
|
||||||
const list = response.data.data.list
|
.filter((item: any) => item.fid)
|
||||||
.filter((item: any) => item.fid)
|
.map((folder: any) => ({
|
||||||
.map((folder: any) => ({
|
fileId: folder.fid,
|
||||||
fileId: folder.fid,
|
fileName: folder.file_name,
|
||||||
fileName: folder.file_name,
|
fileIdToken: folder.share_fid_token,
|
||||||
fileIdToken: folder.share_fid_token,
|
}));
|
||||||
}));
|
return {
|
||||||
return {
|
list,
|
||||||
list,
|
pwdId,
|
||||||
pwdId,
|
stoken: stoken,
|
||||||
stoken: stoken,
|
};
|
||||||
};
|
} else {
|
||||||
} else {
|
return {
|
||||||
return {
|
list: [],
|
||||||
list: [],
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFolderList(parentCid = "0") {
|
async getFolderList(parentCid = "0") {
|
||||||
try {
|
const response = await this.api.get("/1/clouddrive/file/sort", {
|
||||||
const response = await this.api.get("/1/clouddrive/file/sort", {
|
params: {
|
||||||
params: {
|
pr: "ucpro",
|
||||||
pr: "ucpro",
|
fr: "pc",
|
||||||
fr: "pc",
|
uc_param_str: "",
|
||||||
uc_param_str: "",
|
pdir_fid: parentCid,
|
||||||
pdir_fid: parentCid,
|
_page: "1",
|
||||||
_page: "1",
|
_size: "100",
|
||||||
_size: "100",
|
_fetch_total: "false",
|
||||||
_fetch_total: "false",
|
_fetch_sub_dirs: "1",
|
||||||
_fetch_sub_dirs: "1",
|
_sort: "",
|
||||||
_sort: "",
|
__dt: "2093126",
|
||||||
__dt: "2093126",
|
__t: Date.now(),
|
||||||
__t: Date.now(),
|
},
|
||||||
},
|
});
|
||||||
});
|
if (response.data?.data && response.data.data.list.length) {
|
||||||
if (response.data?.data && response.data.data.list.length) {
|
const data = response.data.data.list
|
||||||
return {
|
.filter((item: any) => item.fid && item.file_type === 0)
|
||||||
success: true,
|
.map((folder: any) => ({
|
||||||
data: response.data.data.list
|
cid: folder.fid,
|
||||||
.filter((item: any) => item.fid)
|
name: folder.file_name,
|
||||||
.map((folder: any) => ({
|
path: [],
|
||||||
cid: folder.fid,
|
}));
|
||||||
name: folder.file_name,
|
|
||||||
path: [],
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
Logger.error("获取目录列表失败:", response.data.error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "获取夸克目录列表失败:" + response.data.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error("获取目录列表失败:", error);
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
data,
|
||||||
error: "获取夸克目录列表失败",
|
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
const message = "获取夸克目录列表失败:" + response.data.error;
|
||||||
|
Logger.error(message);
|
||||||
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,15 +148,11 @@ export class QuarkService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: response.data.code === 0,
|
message: response.data.message,
|
||||||
error: response.data.message,
|
|
||||||
data: response.data.data,
|
data: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
throw new Error(error instanceof Error ? error.message : "未知错误");
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "未知错误",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AxiosInstance, AxiosHeaders } from "axios";
|
|||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
import { data } from "cheerio/dist/commonjs/api/attributes";
|
||||||
|
|
||||||
interface RSSItem {
|
interface RSSItem {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -81,7 +82,10 @@ export class RSSSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResults;
|
return {
|
||||||
|
data: allResults,
|
||||||
|
message: "搜索成功",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchInRSSFeed(rssUrl: string) {
|
async searchInRSSFeed(rssUrl: string) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
import { GlobalSettingAttributes } from "../models/GlobalSetting";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { Logger } from "../utils/logger";
|
import { Logger } from "../utils/logger";
|
||||||
@@ -7,19 +9,30 @@ import { Logger } from "../utils/logger";
|
|||||||
interface sourceItem {
|
interface sourceItem {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
completeTitle?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
pubDate?: string;
|
pubDate?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
cloudLinks?: string[];
|
cloudLinks?: string[];
|
||||||
|
tags?: string[];
|
||||||
cloudType?: string;
|
cloudType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Searcher {
|
export class Searcher {
|
||||||
private axiosInstance: AxiosInstance;
|
private axiosInstance: AxiosInstance | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.initializeAxiosInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeAxiosInstance(isUpdate = false) {
|
||||||
|
let settings = null;
|
||||||
|
if (isUpdate) {
|
||||||
|
settings = await GlobalSetting.findOne();
|
||||||
|
}
|
||||||
|
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
|
||||||
this.axiosInstance = createAxiosInstance(
|
this.axiosInstance = createAxiosInstance(
|
||||||
config.telegram.baseUrl,
|
config.telegram.baseUrl,
|
||||||
AxiosHeaders.from({
|
AxiosHeaders.from({
|
||||||
@@ -37,9 +50,15 @@ export class Searcher {
|
|||||||
"sec-fetch-user": "?1",
|
"sec-fetch-user": "?1",
|
||||||
"upgrade-insecure-requests": "1",
|
"upgrade-insecure-requests": "1",
|
||||||
}),
|
}),
|
||||||
true
|
globalSetting?.isProxyEnabled,
|
||||||
|
globalSetting?.isProxyEnabled
|
||||||
|
? { host: globalSetting?.httpProxyHost, port: globalSetting?.httpProxyPort }
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
public async updateAxiosInstance() {
|
||||||
|
await this.initializeAxiosInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
||||||
const links: string[] = [];
|
const links: string[] = [];
|
||||||
@@ -82,22 +101,38 @@ export class Searcher {
|
|||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
allResults.push(...channelResults);
|
allResults.push({
|
||||||
|
list: channelResults,
|
||||||
|
channelInfo: {
|
||||||
|
...channel,
|
||||||
|
channelLogo: results.channelLogo,
|
||||||
|
},
|
||||||
|
id: channel.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allResults;
|
return {
|
||||||
|
data: allResults,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchInWeb(url: string, channelId: string) {
|
async searchInWeb(url: string, channelId: string) {
|
||||||
try {
|
try {
|
||||||
|
if (!this.axiosInstance) {
|
||||||
|
throw new Error("Axios instance is not initialized");
|
||||||
|
}
|
||||||
const response = await this.axiosInstance.get(url);
|
const response = await this.axiosInstance.get(url);
|
||||||
const html = response.data;
|
const html = response.data;
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const items: sourceItem[] = [];
|
const items: sourceItem[] = [];
|
||||||
|
let channelLogo = "";
|
||||||
|
$(".tgme_header_link").each((_, element) => {
|
||||||
|
channelLogo = $(element).find("img").attr("src") || "";
|
||||||
|
});
|
||||||
// 遍历每个消息容器
|
// 遍历每个消息容器
|
||||||
$(".tgme_widget_message_wrap").each((_, element) => {
|
$(".tgme_widget_message_wrap").each((_, element) => {
|
||||||
const messageEl = $(element);
|
const messageEl = $(element);
|
||||||
@@ -109,8 +144,24 @@ export class Searcher {
|
|||||||
?.toString()
|
?.toString()
|
||||||
.split("/")[1];
|
.split("/")[1];
|
||||||
|
|
||||||
// 提取标题 (消息截取100长度)
|
// 提取标题 (第一个<br>标签前的内容)
|
||||||
const title = messageEl.find(".js-message_text").text().trim().substring(0, 50) + "...";
|
const title =
|
||||||
|
messageEl
|
||||||
|
.find(".js-message_text")
|
||||||
|
.html()
|
||||||
|
?.split("<br>")[0]
|
||||||
|
.replace(/<[^>]+>/g, "")
|
||||||
|
.replace(/\n/g, "") || "";
|
||||||
|
|
||||||
|
// 提取描述 (第一个<a>标签前面的内容,不包含标题)
|
||||||
|
const content =
|
||||||
|
messageEl
|
||||||
|
.find(".js-message_text")
|
||||||
|
.html()
|
||||||
|
?.replace(title, "")
|
||||||
|
.split("<a")[0]
|
||||||
|
.replace(/<br>/g, "")
|
||||||
|
.trim() || "";
|
||||||
|
|
||||||
// 提取链接 (消息中的链接)
|
// 提取链接 (消息中的链接)
|
||||||
// const link = messageEl.find('.tgme_widget_message').data('post');
|
// const link = messageEl.find('.tgme_widget_message').data('post');
|
||||||
@@ -118,23 +169,24 @@ export class Searcher {
|
|||||||
// 提取发布时间
|
// 提取发布时间
|
||||||
const pubDate = messageEl.find("time").attr("datetime");
|
const pubDate = messageEl.find("time").attr("datetime");
|
||||||
|
|
||||||
// 提取内容 (完整消息文本)
|
|
||||||
const content = messageEl.find(".js-message_text").text();
|
|
||||||
|
|
||||||
// 提取描述 (消息文本中"描述:"后的内容)
|
|
||||||
const description = content.split("描述:")[1]?.split("\n")[0]?.trim();
|
|
||||||
|
|
||||||
// 提取图片
|
// 提取图片
|
||||||
const image = messageEl
|
const image = messageEl
|
||||||
.find(".tgme_widget_message_photo_wrap")
|
.find(".tgme_widget_message_photo_wrap")
|
||||||
.attr("style")
|
.attr("style")
|
||||||
?.match(/url\('(.+?)'\)/)?.[1];
|
?.match(/url\('(.+?)'\)/)?.[1];
|
||||||
|
|
||||||
|
const tags: string[] = [];
|
||||||
// 提取云盘链接
|
// 提取云盘链接
|
||||||
const links = messageEl
|
const links = messageEl
|
||||||
.find(".tgme_widget_message_text a")
|
.find(".tgme_widget_message_text a")
|
||||||
.map((_, el) => $(el).attr("href"))
|
.map((_, el) => $(el).attr("href"))
|
||||||
.get();
|
.get();
|
||||||
|
messageEl.find(".tgme_widget_message_text a").each((index, element) => {
|
||||||
|
const tagText = $(element).text();
|
||||||
|
if (tagText && tagText.startsWith("#")) {
|
||||||
|
tags.push(tagText);
|
||||||
|
}
|
||||||
|
});
|
||||||
const cloudInfo = this.extractCloudLinks(links.join(" "));
|
const cloudInfo = this.extractCloudLinks(links.join(" "));
|
||||||
// 添加到数组第一位
|
// 添加到数组第一位
|
||||||
items.unshift({
|
items.unshift({
|
||||||
@@ -142,18 +194,21 @@ export class Searcher {
|
|||||||
title,
|
title,
|
||||||
pubDate,
|
pubDate,
|
||||||
content,
|
content,
|
||||||
description,
|
|
||||||
image,
|
image,
|
||||||
cloudLinks: cloudInfo.links,
|
cloudLinks: cloudInfo.links,
|
||||||
cloudType: cloudInfo.cloudType,
|
cloudType: cloudInfo.cloudType,
|
||||||
|
tags,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { items };
|
return { items: items, channelLogo };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`RSS源解析错误: ${url}`, error);
|
Logger.error(`搜索错误: ${url}`, error);
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
|
channelLogo: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default new Searcher();
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ export interface ShareInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareInfoResponse {
|
export interface ShareInfoResponse {
|
||||||
success: boolean;
|
data?: ShareInfo[];
|
||||||
data?: {
|
message?: string;
|
||||||
list: ShareInfo[];
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
backend/src/types/express.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
declare module "express" {
|
||||||
|
interface Request {
|
||||||
|
user?: {
|
||||||
|
userId: string;
|
||||||
|
role: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
|
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
|
||||||
import tunnel from "tunnel";
|
import tunnel from "tunnel";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
|
import GlobalSetting from "../models/GlobalSetting";
|
||||||
|
|
||||||
|
interface ProxyConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function createAxiosInstance(
|
export function createAxiosInstance(
|
||||||
baseURL: string,
|
baseURL: string,
|
||||||
headers: AxiosRequestHeaders,
|
headers: AxiosRequestHeaders,
|
||||||
useProxy: boolean = false
|
useProxy: boolean = false,
|
||||||
|
proxyConfig?: ProxyConfig
|
||||||
): AxiosInstance {
|
): AxiosInstance {
|
||||||
let agent;
|
let agent;
|
||||||
|
console.log(proxyConfig);
|
||||||
if (useProxy) {
|
if (useProxy && proxyConfig) {
|
||||||
agent = tunnel.httpsOverHttp({
|
agent = tunnel.httpsOverHttp({
|
||||||
proxy: {
|
proxy: proxyConfig,
|
||||||
host: config.httpProxy.host,
|
|
||||||
port: Number(config.httpProxy.port),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
backend/src/utils/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { config } from "../config";
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserIdFromToken(req: Request): string | null {
|
||||||
|
try {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Token not found");
|
||||||
|
}
|
||||||
|
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
|
||||||
|
return decoded.userId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Invalid token:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/utils/response.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
code?: number; // 业务状态码
|
||||||
|
message?: string;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendSuccess = (res: Response, response: ResponseData, businessCode: number = 0) => {
|
||||||
|
response.code = businessCode;
|
||||||
|
res.status(200).json(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendError = (res: Response, response: ResponseData, businessCode: number = 10000) => {
|
||||||
|
response.code = businessCode;
|
||||||
|
res.status(200).json(response);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
docs/images/20250220115710.jpg
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
docs/images/hotmovie.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
docs/images/login.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/images/register.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/images/save.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
docs/images/search-movie.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/images/search.png
Normal file
|
After Width: | Height: | Size: 824 KiB |
BIN
docs/images/setting.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
2
frontend/auto-imports.d.ts
vendored
@@ -5,5 +5,5 @@
|
|||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
|
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||||
}
|
}
|
||||||
|
|||||||
28
frontend/components.d.ts
vendored
@@ -7,22 +7,44 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
|
||||||
|
DoubanMovie: typeof import('./src/components/Home/DoubanMovie.vue')['default']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
|
ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"]
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
FolderSelect: typeof import('./src/components/FolderSelect.vue')['default']
|
FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']
|
||||||
ResourceList: typeof import('./src/components/ResourceList.vue')['default']
|
ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']
|
||||||
|
ResourceList: typeof import('./src/components/Home/ResourceList.vue')['default']
|
||||||
|
ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
||||||
|
|||||||
BIN
frontend/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -3,7 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<!-- 移动端适配 -->
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
<title>CloudSaver</title>
|
<title>CloudSaver</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.25",
|
"@types/node": "^20.11.25",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"sass": "^1.83.4",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
|||||||
@@ -5,7 +5,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--theme-color: #3e3e3e;
|
||||||
|
--theme-theme: #133ab3;
|
||||||
|
--theme-background: #fafafa;
|
||||||
|
--theme-other_background: #ffffff;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family:
|
||||||
|
v-sans,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
Segoe UI,
|
||||||
|
sans-serif,
|
||||||
|
"Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji",
|
||||||
|
Segoe UI Symbol;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--theme-color);
|
||||||
|
background-color: var(--theme-background);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import request from "@/utils/request";
|
|||||||
import type { ShareInfoResponse, Folder, Save115FileParams } from "@/types";
|
import type { ShareInfoResponse, Folder, Save115FileParams } from "@/types";
|
||||||
|
|
||||||
export const cloud115Api = {
|
export const cloud115Api = {
|
||||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(shareCode: string, receiveCode = "") {
|
||||||
const { data } = await request.get("/api/cloud115/share-info", {
|
const { data } = await request.get<ShareInfoResponse>("/api/cloud115/share-info", {
|
||||||
params: { shareCode, receiveCode },
|
params: { shareCode, receiveCode },
|
||||||
});
|
});
|
||||||
return data;
|
return data as ShareInfoResponse;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(parentCid = "0"): Promise<{ data: Folder[] }> {
|
async getFolderList(parentCid = "0") {
|
||||||
const { data } = await request.get("/api/cloud115/folders", {
|
const res = await request.get<Folder[]>("/api/cloud115/folders", {
|
||||||
params: { parentCid },
|
params: { parentCid },
|
||||||
});
|
});
|
||||||
return data;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(params: Save115FileParams) {
|
async saveFile(params: Save115FileParams) {
|
||||||
const { data } = await request.post("/api/cloud115/save", params);
|
const res = await request.post("/api/cloud115/save", params);
|
||||||
return data;
|
return res;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
11
frontend/src/api/douban.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
import { HotListItem, HotListParams } from "@/types/douban";
|
||||||
|
|
||||||
|
export const doubanApi = {
|
||||||
|
async getHotList(params: HotListParams) {
|
||||||
|
const { data } = await request.get<HotListItem[]>("/api/douban/hot", {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -2,22 +2,21 @@ import request from "@/utils/request";
|
|||||||
import type { ShareInfoResponse, Folder, SaveQuarkFileParams } from "@/types";
|
import type { ShareInfoResponse, Folder, SaveQuarkFileParams } from "@/types";
|
||||||
|
|
||||||
export const quarkApi = {
|
export const quarkApi = {
|
||||||
async getShareInfo(pwdId: string, passcode = ""): Promise<ShareInfoResponse> {
|
async getShareInfo(pwdId: string, passcode = "") {
|
||||||
const { data } = await request.get("/api/quark/share-info", {
|
const { data } = await request.get<ShareInfoResponse>("/api/quark/share-info", {
|
||||||
params: { pwdId, passcode },
|
params: { pwdId, passcode },
|
||||||
});
|
});
|
||||||
return data;
|
return data as ShareInfoResponse;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFolderList(parentCid = "0"): Promise<{ data: Folder[] }> {
|
async getFolderList(parentCid = "0") {
|
||||||
const { data } = await request.get("/api/quark/folders", {
|
const data = await request.get<Folder[]>("/api/quark/folders", {
|
||||||
params: { parentCid },
|
params: { parentCid },
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveFile(params: SaveQuarkFileParams) {
|
async saveFile(params: SaveQuarkFileParams) {
|
||||||
const { data } = await request.post("/api/quark/save", params);
|
return await request.post("/api/quark/save", params);
|
||||||
return data;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import request from "@/utils/request";
|
|||||||
import type { Resource } from "@/types/index";
|
import type { Resource } from "@/types/index";
|
||||||
|
|
||||||
export const resourceApi = {
|
export const resourceApi = {
|
||||||
search(keyword: string, backupPlan: boolean, channelId?: string, lastMessageId?: string) {
|
search(keyword: string, channelId?: string, lastMessageId?: string) {
|
||||||
return request.get<Resource[]>(`/api/${backupPlan ? "rssSearch" : "search"}`, {
|
return request.get<Resource[]>(`/api/search`, {
|
||||||
params: { keyword, channelId, lastMessageId },
|
params: { keyword, channelId, lastMessageId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
17
frontend/src/api/setting.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
import type { GlobalSettingAttributes, UserSettingAttributes } from "@/types";
|
||||||
|
|
||||||
|
export const settingApi = {
|
||||||
|
getSetting: () => {
|
||||||
|
return request.get<{
|
||||||
|
userSettings: UserSettingAttributes;
|
||||||
|
globalSetting: GlobalSettingAttributes;
|
||||||
|
}>("/api/setting/get");
|
||||||
|
},
|
||||||
|
saveSetting: (data: {
|
||||||
|
userSettings: UserSettingAttributes;
|
||||||
|
globalSetting?: GlobalSettingAttributes | null;
|
||||||
|
}) => {
|
||||||
|
return request.post("/api/setting/save", data);
|
||||||
|
},
|
||||||
|
};
|
||||||
10
frontend/src/api/user.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
login: (data: { username: string; password: string }) => {
|
||||||
|
return request.post<{ token: string }>("/api/user/login", data);
|
||||||
|
},
|
||||||
|
register: (data: { username: string; password: string }) => {
|
||||||
|
return request.post<{ token: string }>("/api/user/register", data);
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
frontend/src/assets/images/login-bg.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/src/assets/images/logo-1.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
frontend/src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
155
frontend/src/components/AsideMenu.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="aside-menu">
|
||||||
|
<div class="logo">
|
||||||
|
<img :src="logo" class="logo-img" />
|
||||||
|
<div class="logo-text">Cloud Saver</div>
|
||||||
|
</div>
|
||||||
|
<el-menu
|
||||||
|
:default-active="currentMenu?.index || '1'"
|
||||||
|
:default-openeds="currentMenuOpen"
|
||||||
|
class="el-menu-vertical"
|
||||||
|
@open="handleOpen"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<template v-for="menu in menuList">
|
||||||
|
<el-sub-menu :index="menu.index" v-if="menu.children">
|
||||||
|
<template #title>
|
||||||
|
<el-icon><component :is="menu.icon" /></el-icon>
|
||||||
|
<span>{{ menu.title }}</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="child in menu.children"
|
||||||
|
:index="child.index"
|
||||||
|
:key="child.index"
|
||||||
|
@click="handleMenuClick(child)"
|
||||||
|
>
|
||||||
|
{{ child.title }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
<el-menu-item
|
||||||
|
v-else
|
||||||
|
:index="menu.index"
|
||||||
|
@click="handleMenuClick(menu)"
|
||||||
|
:disabled="menu.disabled"
|
||||||
|
>
|
||||||
|
<el-icon><component :is="menu.icon" /></el-icon>
|
||||||
|
<span>{{ menu.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Search, Film, Setting } from "@element-plus/icons-vue";
|
||||||
|
import logo from "@/assets/images/logo.png";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { computed } from "vue";
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
interface MenuItem {
|
||||||
|
index: string;
|
||||||
|
title: string;
|
||||||
|
icon?: any;
|
||||||
|
router?: string;
|
||||||
|
children?: MenuItem[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuList: MenuItem[] = [
|
||||||
|
{
|
||||||
|
index: "2",
|
||||||
|
title: "资源搜索",
|
||||||
|
icon: Search,
|
||||||
|
router: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: "1",
|
||||||
|
title: "豆瓣榜单",
|
||||||
|
icon: Film,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: "1-1",
|
||||||
|
title: "热门电影",
|
||||||
|
router: "/douban?type=movie",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: "1-2",
|
||||||
|
title: "热门电视剧",
|
||||||
|
router: "/douban?type=tv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: "1-3",
|
||||||
|
title: "最新电影",
|
||||||
|
router: "/douban?type=movie&tag=最新",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: "1-4",
|
||||||
|
title: "热门综艺",
|
||||||
|
router: "/douban?type=tv&tag=综艺",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: "3",
|
||||||
|
title: "设置",
|
||||||
|
icon: Setting,
|
||||||
|
router: "/setting",
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentMenu = computed(() => {
|
||||||
|
console.log("route", decodeURIComponent(route.fullPath));
|
||||||
|
return menuList
|
||||||
|
.reduce((pre: MenuItem[], cur: MenuItem) => {
|
||||||
|
if (!cur.children) {
|
||||||
|
pre.push(cur);
|
||||||
|
} else {
|
||||||
|
pre.push(...cur.children);
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
}, [])
|
||||||
|
.find((x) => x.router === decodeURIComponent(route.fullPath));
|
||||||
|
});
|
||||||
|
const currentMenuOpen = computed(() => {
|
||||||
|
if (currentMenu.value && currentMenu.value.index.length > 1) {
|
||||||
|
console.log([currentMenu.value.index.split("-")[0]]);
|
||||||
|
return [currentMenu.value.index.split("-")[0]];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const handleOpen = (key: string, keyPath: string[]) => {
|
||||||
|
console.log(key, keyPath);
|
||||||
|
};
|
||||||
|
const handleClose = (key: string, keyPath: string[]) => {
|
||||||
|
console.log(key, keyPath);
|
||||||
|
};
|
||||||
|
const handleMenuClick = (menu: any) => {
|
||||||
|
console.log(menu);
|
||||||
|
if (menu.router) {
|
||||||
|
router.push(menu.router);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-menu-vertical {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
.logo-img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.logo-text {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
0
frontend/src/components/Home/DoubanMovie.vue
Normal file
@@ -30,6 +30,7 @@
|
|||||||
import { quarkApi } from "@/api/quark";
|
import { quarkApi } from "@/api/quark";
|
||||||
import type { TreeInstance } from "element-plus";
|
import type { TreeInstance } from "element-plus";
|
||||||
import type { Folder } from "@/types";
|
import type { Folder } from "@/types";
|
||||||
|
import { type RequestResult } from "@/types/response";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -58,13 +59,10 @@
|
|||||||
quark: quarkApi,
|
quark: quarkApi,
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadNode = async (node: any, resolve: (data: Folder[]) => void) => {
|
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
|
||||||
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
|
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
|
||||||
try {
|
try {
|
||||||
let res: {
|
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
|
||||||
data: Folder[];
|
|
||||||
error?: string;
|
|
||||||
} = { data: [] };
|
|
||||||
if (node.level === 0) {
|
if (node.level === 0) {
|
||||||
if (api.getFolderList) {
|
if (api.getFolderList) {
|
||||||
// 使用类型保护检查方法是否存在
|
// 使用类型保护检查方法是否存在
|
||||||
@@ -76,11 +74,10 @@
|
|||||||
res = await api.getFolderList(node.data.cid);
|
res = await api.getFolderList(node.data.cid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (res.data?.length > 0) {
|
if (res?.code === 0) {
|
||||||
resolve(res.data);
|
resolve(res.data.length ? res.data : []);
|
||||||
} else {
|
} else {
|
||||||
resolve([]);
|
throw new Error(res.message);
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
|
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
|
||||||
189
frontend/src/components/Home/ResourceCard.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-card-list">
|
||||||
|
<div v-for="group in store.resources" :key="group.id" class="resource-list">
|
||||||
|
<div class="group-header">
|
||||||
|
<el-link :href="`https://t.me/s/${group.id}`" target="_blank" :underline="false">
|
||||||
|
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
|
||||||
|
{{ group.channelInfo.name }}</el-link
|
||||||
|
>
|
||||||
|
<el-icon class="header-icon" @click="group.displayList = !group.displayList"
|
||||||
|
><ArrowDown
|
||||||
|
/></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-item-list" v-show="group.displayList">
|
||||||
|
<div v-for="resource in group.list" :key="resource.messageId" class="card-item-content">
|
||||||
|
<el-card class="card-item">
|
||||||
|
<el-image
|
||||||
|
class="card-item-image"
|
||||||
|
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`"
|
||||||
|
fit="cover"
|
||||||
|
lazy
|
||||||
|
:alt="resource.title"
|
||||||
|
hide-on-click-modal
|
||||||
|
:preview-src-list="[
|
||||||
|
`${location.origin}/tele-images/?url=${encodeURIComponent(resource.image as string)}`,
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<el-link :href="resource.cloudLinks[0]" target="_blank" :underline="false"
|
||||||
|
><div class="item-name">{{ resource.title }}</div></el-link
|
||||||
|
>
|
||||||
|
<div class="item-description" v-html="resource.content"></div>
|
||||||
|
<div class="tags-list" v-if="resource.tags && resource.tags.length">
|
||||||
|
<span>标签:</span>
|
||||||
|
<el-tag
|
||||||
|
v-for="item in resource.tags"
|
||||||
|
class="resource_tag"
|
||||||
|
:key="item"
|
||||||
|
@click="searchMovieforTag(item)"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="item-footer">
|
||||||
|
<el-tag
|
||||||
|
:type="store.tagColor[resource.cloudType as keyof TagColor]"
|
||||||
|
effect="dark"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ resource.cloudType }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button @click="handleSave(resource)">转存</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="load-more" v-show="group.displayList">
|
||||||
|
<el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useResourceStore } from "@/stores/resource";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { ResourceItem, TagColor } from "@/types";
|
||||||
|
|
||||||
|
const store = useResourceStore();
|
||||||
|
|
||||||
|
const location = computed(() => window.location);
|
||||||
|
|
||||||
|
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
|
||||||
|
|
||||||
|
const handleSave = (resource: ResourceItem) => {
|
||||||
|
emit("save", resource);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMovieforTag = (tag: string) => {
|
||||||
|
emit("searchMovieforTag", tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadMore = (channelId: string) => {
|
||||||
|
emit("loadMore", channelId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.resource-list {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--theme-other_background);
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-item-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 220px);
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
/* grid-column-gap: auto-fill; */
|
||||||
|
/* flex-wrap: wrap; */
|
||||||
|
}
|
||||||
|
.card-item-content {
|
||||||
|
/* height: 520px; */
|
||||||
|
}
|
||||||
|
.channel-logo {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.load-more {
|
||||||
|
margin-top: 40px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.card-item {
|
||||||
|
max-width: 480px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.card-item-image {
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
.item-name,
|
||||||
|
.item-description {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 15px 0;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: all;
|
||||||
|
}
|
||||||
|
.item-description {
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
margin-top: 0;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
.item-name {
|
||||||
|
height: 58px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.tags-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 58px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.resource_tag {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.group-header {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
/* text-align: center; */
|
||||||
|
.el-link {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.header-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
frontend/src/components/Home/ResourceTable.vue
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<el-table
|
||||||
|
v-loading="store.loading"
|
||||||
|
class="resource-list-table"
|
||||||
|
:data="store.resources"
|
||||||
|
style="width: 100%"
|
||||||
|
row-key="id"
|
||||||
|
:default-expand-all="true"
|
||||||
|
>
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="props">
|
||||||
|
<el-table :data="props.row.list" style="width: 100%">
|
||||||
|
<el-table-column label="图片" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
class="table-item-image"
|
||||||
|
v-if="row.image"
|
||||||
|
:src="`/tele-images/?url=${encodeURIComponent(row.image as string)}`"
|
||||||
|
hide-on-click-modal
|
||||||
|
:preview-src-list="[
|
||||||
|
`${location.origin}/tele-images/?url=${encodeURIComponent(row.image as string)}`,
|
||||||
|
]"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
:max-scale="7"
|
||||||
|
:min-scale="0.2"
|
||||||
|
:initial-index="4"
|
||||||
|
preview-teleported
|
||||||
|
:z-index="999"
|
||||||
|
fit="cover"
|
||||||
|
width="60"
|
||||||
|
height="90"
|
||||||
|
/>
|
||||||
|
<el-icon v-else size="20"><Close /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="标题" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :href="row.cloudLinks[0]" target="_blank">
|
||||||
|
{{ row.title }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="描述">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-html="row.content" class="item-description"></div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="tags" label="标签">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="tags-list" v-if="row.tags.length > 0">
|
||||||
|
<span>标签:</span>
|
||||||
|
<el-tag
|
||||||
|
v-for="item in row.tags"
|
||||||
|
class="resource_tag"
|
||||||
|
:key="item"
|
||||||
|
@click="searchMovieforTag(item)"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else>无</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- <el-table-column label="地址">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link :href="row.cloudLinks[0]" target="_blank">
|
||||||
|
{{ row.cloudLinks[0] }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column label="云盘类型" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="store.tagColor[row.cloudType as keyof TagColor]" effect="dark" round>
|
||||||
|
{{ row.cloudType }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button @click="handleSave(row)">转存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="load-more">
|
||||||
|
<el-button :loading="props.row.loading" @click="handleLoadMore(props.row.id)">
|
||||||
|
加载更多
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="来源" prop="channel">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="group-header">
|
||||||
|
<el-image :src="row.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
|
||||||
|
<span>{{ row.channelInfo.name }}</span>
|
||||||
|
<span class="item-count">({{ row.list.length }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useResourceStore } from "@/stores/resource";
|
||||||
|
import type { Resource, TagColor } from "@/types";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const store = useResourceStore();
|
||||||
|
|
||||||
|
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
|
||||||
|
|
||||||
|
const location = computed(() => window.location);
|
||||||
|
|
||||||
|
const handleSave = (resource: Resource) => {
|
||||||
|
emit("save", resource);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加加载更多处理函数
|
||||||
|
const handleLoadMore = (channelId: string) => {
|
||||||
|
emit("loadMore", channelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMovieforTag = (tag: string) => {
|
||||||
|
emit("searchMovieforTag", tag);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.resource-list-table {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.channel-logo {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-item-image {
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.tags-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.resource_tag {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.item-description {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 15px 0;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expand-column) {
|
||||||
|
.cell {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expanded-cell) {
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expand-icon) {
|
||||||
|
height: 23px;
|
||||||
|
line-height: 23px;
|
||||||
|
}
|
||||||
|
.load-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="resource-list">
|
|
||||||
<el-table
|
|
||||||
v-loading="store.loading"
|
|
||||||
:data="groupedResources"
|
|
||||||
style="width: 100%"
|
|
||||||
row-key="id"
|
|
||||||
:default-expand-all="true"
|
|
||||||
>
|
|
||||||
<el-table-column type="expand">
|
|
||||||
<template #default="props">
|
|
||||||
<el-table :data="props.row.items" style="width: 100%">
|
|
||||||
<el-table-column label="图片" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-image
|
|
||||||
v-if="row.image"
|
|
||||||
:src="row.image"
|
|
||||||
:preview-src-list="[row.image]"
|
|
||||||
:zoom-rate="1.2"
|
|
||||||
:max-scale="7"
|
|
||||||
:min-scale="0.2"
|
|
||||||
:initial-index="4"
|
|
||||||
preview-teleported
|
|
||||||
:z-index="999"
|
|
||||||
fit="cover"
|
|
||||||
width="60"
|
|
||||||
height="90"
|
|
||||||
/>
|
|
||||||
<el-icon v-else size="20"><Close /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="title" label="标题" width="180" />
|
|
||||||
<el-table-column label="地址">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-link :href="row.cloudLinks[0]" target="_blank">
|
|
||||||
{{ row.cloudLinks[0] }}
|
|
||||||
</el-link>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="云盘类型" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag
|
|
||||||
:type="tagColor[row.cloudType as keyof typeof tagColor]"
|
|
||||||
effect="dark"
|
|
||||||
round
|
|
||||||
>
|
|
||||||
{{ row.cloudType }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button @click="handleSave(row)">转存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div v-if="props.row.hasMore" class="load-more">
|
|
||||||
<el-button :loading="props.row.loading" @click="handleLoadMore(props.row.channel)">
|
|
||||||
加载更多
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="来源" prop="channel">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="group-header">
|
|
||||||
<span>{{ row.channel }}</span>
|
|
||||||
<span class="item-count">({{ row.items.length }})</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-dialog v-model="folderDialogVisible" title="选择保存目录" v-if="currentResource">
|
|
||||||
<template #header="{ titleId }">
|
|
||||||
<div class="my-header">
|
|
||||||
<div :id="titleId">
|
|
||||||
<el-tag
|
|
||||||
:type="tagColor[currentResource.cloudType as keyof typeof tagColor]"
|
|
||||||
effect="dark"
|
|
||||||
round
|
|
||||||
>
|
|
||||||
{{ currentResource.cloudType }}
|
|
||||||
</el-tag>
|
|
||||||
选择保存目录
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<folder-select
|
|
||||||
v-if="folderDialogVisible"
|
|
||||||
@select="handleFolderSelect"
|
|
||||||
@close="folderDialogVisible = false"
|
|
||||||
:cloudType="currentResource.cloudType"
|
|
||||||
/>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="folderDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSaveBtnClick">保存</el-button>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from "vue";
|
|
||||||
import { useResourceStore } from "@/stores/resource";
|
|
||||||
import FolderSelect from "./FolderSelect.vue";
|
|
||||||
import type { Resource } from "@/types";
|
|
||||||
|
|
||||||
const tagColor = {
|
|
||||||
baiduPan: "primary",
|
|
||||||
weiyun: "info",
|
|
||||||
aliyun: "warning",
|
|
||||||
pan115: "danger",
|
|
||||||
quark: "success",
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = useResourceStore();
|
|
||||||
const folderDialogVisible = ref(false);
|
|
||||||
const currentResource = ref<Resource | null>(null);
|
|
||||||
const currentFolderId = ref<string | null>(null);
|
|
||||||
|
|
||||||
// 按来源分组的数据
|
|
||||||
const groupedResources = computed(() => {
|
|
||||||
const groups = store.resources.reduce(
|
|
||||||
(acc, curr) => {
|
|
||||||
const channel = curr.channel;
|
|
||||||
const channelId = curr.channelId;
|
|
||||||
if (!acc[channel]) {
|
|
||||||
acc[channel] = {
|
|
||||||
channel,
|
|
||||||
items: [],
|
|
||||||
hasMore: true,
|
|
||||||
loading: false, // 添加 loading 状态
|
|
||||||
id: channelId || "", // 用于row-key
|
|
||||||
};
|
|
||||||
}
|
|
||||||
acc[channel].items.push(curr);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<
|
|
||||||
string,
|
|
||||||
{ channel: string; items: Resource[]; id: string; hasMore: boolean; loading: boolean }
|
|
||||||
>
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.values(groups);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = (resource: Resource) => {
|
|
||||||
currentResource.value = resource;
|
|
||||||
folderDialogVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFolderSelect = async (folderId: string) => {
|
|
||||||
if (!currentResource.value) return;
|
|
||||||
currentFolderId.value = folderId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveBtnClick = async () => {
|
|
||||||
if (!currentResource.value || !currentFolderId.value) return;
|
|
||||||
folderDialogVisible.value = false;
|
|
||||||
await store.saveResource(currentResource.value, currentFolderId.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加加载更多处理函数
|
|
||||||
const handleLoadMore = async (channel: string) => {
|
|
||||||
const group = groupedResources.value.find((g) => g.channel === channel);
|
|
||||||
if (!group || group.loading) return;
|
|
||||||
|
|
||||||
group.loading = true;
|
|
||||||
try {
|
|
||||||
const lastMessageId = group.items[group.items.length - 1].messageId;
|
|
||||||
store.searchResources("", false, true, group.id, lastMessageId);
|
|
||||||
} finally {
|
|
||||||
group.loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.resource-list {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-count {
|
|
||||||
color: #909399;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__expand-column) {
|
|
||||||
.cell {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__expanded-cell) {
|
|
||||||
padding: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__expand-icon) {
|
|
||||||
height: 23px;
|
|
||||||
line-height: 23px;
|
|
||||||
}
|
|
||||||
.load-more {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,70 +1,115 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<el-input
|
<div class="search-bar__input">
|
||||||
v-model="keyword"
|
<input
|
||||||
placeholder="请输入搜索关键词与输入链接直接解析"
|
v-model="keyword"
|
||||||
class="input-with-select"
|
type="text"
|
||||||
@keyup.enter="handleSearch"
|
placeholder="请输入搜索关键词或输入链接直接解析"
|
||||||
style="margin-bottom: 8px"
|
class="input-with-select"
|
||||||
>
|
@keyup.enter="handleSearch"
|
||||||
<template #append>
|
/>
|
||||||
<el-button type="success" @click="handleSearch">{{ searchBtnText }}</el-button>
|
<el-icon class="search_icon" size="28px" @click="handleSearch"><Search /></el-icon class="search_icon">
|
||||||
</template>
|
</div>
|
||||||
</el-input>
|
<div class="logout" alt="退出登录" @click="logout">
|
||||||
<el-alert
|
<el-tooltip
|
||||||
title="可直接输入链接进行资源解析,也可进行资源搜索!"
|
class="box-item"
|
||||||
type="info"
|
effect="dark"
|
||||||
show-icon
|
content="退出登录"
|
||||||
:closable="false"
|
placement="bottom"
|
||||||
/>
|
>
|
||||||
<div class="search-new">
|
<el-icon><SwitchButton class="logout-icon" /></el-icon>
|
||||||
<el-button type="primary" @click="handleSearchNew">最新资源</el-button>
|
</el-tooltip>
|
||||||
<div class="switch-source">
|
|
||||||
<el-switch v-model="backupPlan" /><span class="label">使用rsshub(较慢)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { effect, ref } from "vue";
|
import { computed, ref,watch } from "vue";
|
||||||
import { useResourceStore } from "@/stores/resource";
|
import { useResourceStore } from "@/stores/resource";
|
||||||
|
import { useRoute,useRouter } from "vue-router";
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const resourcStore = useResourceStore();
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const backupPlan = ref(false);
|
const routeKeyword = computed(() => route.query.keyword as string);
|
||||||
const store = useResourceStore();
|
const logout = () => {
|
||||||
const searchBtnText = ref("搜索");
|
localStorage.removeItem("token");
|
||||||
|
router.push({ path: "/login" });
|
||||||
effect(() => {
|
};
|
||||||
// 监听搜索关键词的变化,如果存在,则自动触发搜索
|
|
||||||
if (keyword.value && keyword.value.startsWith("http")) {
|
|
||||||
searchBtnText.value = "解析";
|
|
||||||
} else {
|
|
||||||
searchBtnText.value = "搜索";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
// 如果搜索内容是一个https的链接,则尝试解析链接
|
// 如果搜索内容是一个https的链接,则尝试解析链接
|
||||||
if (keyword.value.startsWith("http")) {
|
if (keyword.value.startsWith("http")) {
|
||||||
store.parsingCloudLink(keyword.value);
|
resourcStore.parsingCloudLink(keyword.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!keyword.value.trim()) {
|
if (!keyword.value.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await store.searchResources(keyword.value, backupPlan.value);
|
await resourcStore.searchResources(keyword.value);
|
||||||
|
if(route.path !== '/'){
|
||||||
|
router.push({ path: '/' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchNew = async () => {
|
watch(() => routeKeyword.value, () => {
|
||||||
keyword.value = "";
|
if(routeKeyword.value){
|
||||||
await store.searchResources("", backupPlan.value);
|
keyword.value = routeKeyword.value;
|
||||||
};
|
handleSearch();
|
||||||
|
} else {
|
||||||
|
keyword.value = ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.search-bar {
|
.search-bar {
|
||||||
padding: 20px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
.logout{
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-left: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-bar__input {
|
||||||
|
width: 100%;
|
||||||
|
margin: 15px auto;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--theme-other_background);
|
||||||
|
box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.input-with-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: none;
|
||||||
|
padding-left: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 40px;
|
||||||
|
line-height: 100%;
|
||||||
|
border: unset;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.search_icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-bar_tips{
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-new {
|
.search-new {
|
||||||
@@ -79,4 +124,7 @@
|
|||||||
.switch-source .label {
|
.switch-source .label {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
.display-style {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import HomeView from "@/views/HomeView.vue";
|
import Login from "@/views/Login.vue";
|
||||||
|
import Home from "@/views/Home.vue";
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home",
|
name: "home",
|
||||||
component: HomeView,
|
component: Home,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "resource",
|
||||||
|
component: () => import("@/views/ResourceList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/douban",
|
||||||
|
name: "douban",
|
||||||
|
component: () => import("@/views/Douban.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/setting",
|
||||||
|
name: "setting",
|
||||||
|
component: () => import("@/views/Setting.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
name: "login",
|
||||||
|
component: Login,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
50
frontend/src/stores/douban.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { doubanApi } from "@/api/douban";
|
||||||
|
import { HotListItem } from "@/types/douban";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
interface StoreType {
|
||||||
|
hotList: HotListItem[];
|
||||||
|
currentParams: CurrentParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentParams {
|
||||||
|
type: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDoubanStore = defineStore("douban", {
|
||||||
|
state: (): StoreType => ({
|
||||||
|
hotList: [],
|
||||||
|
currentParams: {
|
||||||
|
type: "movie",
|
||||||
|
tag: "热门",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getHotList() {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
type: this.currentParams.type,
|
||||||
|
tag: this.currentParams.tag || "热门",
|
||||||
|
page_limit: "20",
|
||||||
|
page_start: "0",
|
||||||
|
};
|
||||||
|
const result = await doubanApi.getHotList(params);
|
||||||
|
if (result && result.length > 0) {
|
||||||
|
this.hotList = result;
|
||||||
|
} else {
|
||||||
|
console.log("获取热门列表失败");
|
||||||
|
ElMessage.warning("获取热门列表失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error || "获取热门列表失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentParams(currentParams: CurrentParams) {
|
||||||
|
this.currentParams = currentParams;
|
||||||
|
this.getHotList();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
17
frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
interface StoreType {
|
||||||
|
scrollTop: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = defineStore("global", {
|
||||||
|
state: (): StoreType => ({
|
||||||
|
scrollTop: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setScrollTop(top: boolean) {
|
||||||
|
this.scrollTop = top;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -2,9 +2,26 @@ import { defineStore } from "pinia";
|
|||||||
import { cloud115Api } from "@/api/cloud115";
|
import { cloud115Api } from "@/api/cloud115";
|
||||||
import { resourceApi } from "@/api/resource";
|
import { resourceApi } from "@/api/resource";
|
||||||
import { quarkApi } from "@/api/quark";
|
import { quarkApi } from "@/api/quark";
|
||||||
import type { Resource, ShareInfoResponse, Save115FileParams, SaveQuarkFileParams } from "@/types";
|
import type {
|
||||||
|
Resource,
|
||||||
|
ShareInfoResponse,
|
||||||
|
Save115FileParams,
|
||||||
|
SaveQuarkFileParams,
|
||||||
|
ResourceItem,
|
||||||
|
} from "@/types";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
interface StorageListObject {
|
||||||
|
list: Resource[];
|
||||||
|
lastUpdateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastResource = (
|
||||||
|
localStorage.getItem("last_resource_list")
|
||||||
|
? JSON.parse(localStorage.getItem("last_resource_list") as string)
|
||||||
|
: { list: [] }
|
||||||
|
) as StorageListObject;
|
||||||
|
|
||||||
// 定义云盘驱动配置类型
|
// 定义云盘驱动配置类型
|
||||||
interface CloudDriveConfig {
|
interface CloudDriveConfig {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -12,7 +29,7 @@ interface CloudDriveConfig {
|
|||||||
regex: RegExp;
|
regex: RegExp;
|
||||||
api: {
|
api: {
|
||||||
getShareInfo: (parsedCode: any) => Promise<ShareInfoResponse>;
|
getShareInfo: (parsedCode: any) => Promise<ShareInfoResponse>;
|
||||||
saveFile: (params: Record<string, any>) => Promise<{ success: boolean; error?: string }>;
|
saveFile: (params: Record<string, any>) => Promise<any>;
|
||||||
};
|
};
|
||||||
parseShareCode: (match: RegExpMatchArray) => Record<string, string>;
|
parseShareCode: (match: RegExpMatchArray) => Record<string, string>;
|
||||||
getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => Record<string, any>;
|
getSaveParams: (shareInfo: ShareInfoResponse, folderId: string) => Record<string, any>;
|
||||||
@@ -23,20 +40,20 @@ export const CLOUD_DRIVES: CloudDriveConfig[] = [
|
|||||||
{
|
{
|
||||||
name: "115网盘",
|
name: "115网盘",
|
||||||
type: "pan115",
|
type: "pan115",
|
||||||
regex: /(?:115|anxia)\.com\/s\/([^?]+)(?:\?password=([^#]+))?/,
|
regex: /(?:115|anxia|115cdn)\.com\/s\/([^?]+)(?:\?password=([^&#]+))?/,
|
||||||
api: {
|
api: {
|
||||||
getShareInfo: (parsedCode: { shareCode: string; receiveCode: string }) =>
|
getShareInfo: (parsedCode: { shareCode: string; receiveCode: string }) =>
|
||||||
cloud115Api.getShareInfo(parsedCode.shareCode, parsedCode.receiveCode),
|
cloud115Api.getShareInfo(parsedCode.shareCode, parsedCode.receiveCode),
|
||||||
saveFile: (params) => cloud115Api.saveFile(params as Save115FileParams),
|
saveFile: async (params) => await cloud115Api.saveFile(params as Save115FileParams),
|
||||||
},
|
},
|
||||||
parseShareCode: (match) => ({
|
parseShareCode: (match) => ({
|
||||||
shareCode: match[1],
|
shareCode: match[1],
|
||||||
receiveCode: match[2] || "",
|
receiveCode: match[2] || "",
|
||||||
}),
|
}),
|
||||||
getSaveParams: (shareInfo, folderId) => ({
|
getSaveParams: (shareInfo, folderId) => ({
|
||||||
shareCode: shareInfo.data.shareCode,
|
shareCode: shareInfo.shareCode,
|
||||||
receiveCode: shareInfo.data.receiveCode,
|
receiveCode: shareInfo.receiveCode,
|
||||||
fileId: shareInfo.data.list[0].fileId,
|
fileId: shareInfo.list[0].fileId,
|
||||||
folderId,
|
folderId,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -46,15 +63,15 @@ export const CLOUD_DRIVES: CloudDriveConfig[] = [
|
|||||||
regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/,
|
regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/,
|
||||||
api: {
|
api: {
|
||||||
getShareInfo: (parsedCode: { pwdId: string }) => quarkApi.getShareInfo(parsedCode.pwdId),
|
getShareInfo: (parsedCode: { pwdId: string }) => quarkApi.getShareInfo(parsedCode.pwdId),
|
||||||
saveFile: (params) => quarkApi.saveFile(params as SaveQuarkFileParams),
|
saveFile: async (params) => await quarkApi.saveFile(params as SaveQuarkFileParams),
|
||||||
},
|
},
|
||||||
parseShareCode: (match) => ({ pwdId: match[1] }),
|
parseShareCode: (match) => ({ pwdId: match[1] }),
|
||||||
getSaveParams: (shareInfo, folderId) => ({
|
getSaveParams: (shareInfo, folderId) => ({
|
||||||
fid_list: shareInfo.data.list.map((item) => item.fileId || ""),
|
fid_list: shareInfo.list.map((item) => item.fileId || ""),
|
||||||
fid_token_list: shareInfo.data.list.map((item) => item.fileIdToken || ""),
|
fid_token_list: shareInfo.list.map((item) => item.fileIdToken || ""),
|
||||||
to_pdir_fid: folderId,
|
to_pdir_fid: folderId,
|
||||||
pwd_id: shareInfo.data.pwdId || "",
|
pwd_id: shareInfo.pwdId || "",
|
||||||
stoken: shareInfo.data.stoken || "",
|
stoken: shareInfo.stoken || "",
|
||||||
pdir_fid: "0",
|
pdir_fid: "0",
|
||||||
scene: "link",
|
scene: "link",
|
||||||
}),
|
}),
|
||||||
@@ -63,7 +80,15 @@ export const CLOUD_DRIVES: CloudDriveConfig[] = [
|
|||||||
|
|
||||||
export const useResourceStore = defineStore("resource", {
|
export const useResourceStore = defineStore("resource", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
resources: [] as Resource[],
|
tagColor: {
|
||||||
|
baiduPan: "primary",
|
||||||
|
weiyun: "info",
|
||||||
|
aliyun: "warning",
|
||||||
|
pan115: "danger",
|
||||||
|
quark: "success",
|
||||||
|
},
|
||||||
|
resources: lastResource.list,
|
||||||
|
lastUpdateTime: lastResource.lastUpdateTime || "",
|
||||||
selectedResources: [] as Resource[],
|
selectedResources: [] as Resource[],
|
||||||
loading: false,
|
loading: false,
|
||||||
lastKeyWord: "",
|
lastKeyWord: "",
|
||||||
@@ -72,65 +97,64 @@ export const useResourceStore = defineStore("resource", {
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
// 搜索资源
|
// 搜索资源
|
||||||
async searchResources(
|
async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise<void> {
|
||||||
keyword?: string,
|
|
||||||
backupPlan = false,
|
|
||||||
isLoadMore = false,
|
|
||||||
channelId?: string,
|
|
||||||
lastMessageId?: string
|
|
||||||
): Promise<void> {
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (!isLoadMore) this.resources = [];
|
if (!isLoadMore) this.resources = [];
|
||||||
try {
|
try {
|
||||||
|
let lastMessageId = "";
|
||||||
if (isLoadMore) {
|
if (isLoadMore) {
|
||||||
|
const list = this.resources.find((x) => x.id === channelId)?.list || [];
|
||||||
|
lastMessageId = list[list.length - 1].messageId || "";
|
||||||
if (!lastMessageId) {
|
if (!lastMessageId) {
|
||||||
ElMessage.error("当次搜索源不支持加载更多");
|
ElMessage.error("当次搜索源不支持加载更多");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keyword = this.lastKeyWord;
|
keyword = this.lastKeyWord;
|
||||||
backupPlan = this.backupPlan;
|
|
||||||
}
|
}
|
||||||
const { data } = await resourceApi.search(
|
let { data = [] } = await resourceApi.search(keyword || "", channelId, lastMessageId);
|
||||||
keyword || "",
|
data = data.filter((item) => item.list.length > 0);
|
||||||
backupPlan,
|
|
||||||
channelId,
|
|
||||||
lastMessageId
|
|
||||||
);
|
|
||||||
this.lastKeyWord = keyword || "";
|
this.lastKeyWord = keyword || "";
|
||||||
if (isLoadMore) {
|
if (isLoadMore) {
|
||||||
this.resources.push(
|
const findedIndex = this.resources.findIndex((item) => item.id === data[0]?.id);
|
||||||
...data.filter(
|
if (findedIndex !== -1) {
|
||||||
(item) => !this.selectedResources.some((selectedItem) => selectedItem.id === item.id)
|
this.resources[findedIndex].list.push(...data[0].list);
|
||||||
)
|
}
|
||||||
);
|
if (data.length === 0) {
|
||||||
|
ElMessage.warning("没有更多了~");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.resources = data;
|
this.resources = data.map((item) => ({ ...item, displayList: true }));
|
||||||
|
if (this.resources.length === 0) {
|
||||||
|
ElMessage.warning("未搜索到相关资源");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 获取当前时间字符串 用于存储到本地
|
||||||
|
this.lastUpdateTime = new Date().toLocaleString();
|
||||||
|
localStorage.setItem(
|
||||||
|
"last_resource_list",
|
||||||
|
JSON.stringify({ list: this.resources, lastUpdateTime: this.lastUpdateTime })
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError("搜索失败,请重试", error);
|
this.handleError("搜索失败,请重试", null);
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 转存资源
|
// 转存资源
|
||||||
async saveResource(resource: Resource, folderId: string): Promise<void> {
|
async saveResource(resource: ResourceItem, folderId: string): Promise<void> {
|
||||||
try {
|
const savePromises: Promise<void>[] = [];
|
||||||
const savePromises: Promise<void>[] = [];
|
CLOUD_DRIVES.forEach((drive) => {
|
||||||
CLOUD_DRIVES.forEach((drive) => {
|
if (resource.cloudLinks.some((link) => drive.regex.test(link))) {
|
||||||
if (resource.cloudLinks.some((link) => drive.regex.test(link))) {
|
savePromises.push(this.saveResourceToDrive(resource, folderId, drive));
|
||||||
savePromises.push(this.saveResourceToDrive(resource, folderId, drive));
|
}
|
||||||
}
|
});
|
||||||
});
|
await Promise.all(savePromises);
|
||||||
await Promise.all(savePromises);
|
|
||||||
} catch (error) {
|
|
||||||
this.handleError("转存失败,请重试", error);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 保存资源到网盘
|
// 保存资源到网盘
|
||||||
async saveResourceToDrive(
|
async saveResourceToDrive(
|
||||||
resource: Resource,
|
resource: ResourceItem,
|
||||||
folderId: string,
|
folderId: string,
|
||||||
drive: CloudDriveConfig
|
drive: CloudDriveConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -141,27 +165,28 @@ export const useResourceStore = defineStore("resource", {
|
|||||||
if (!match) throw new Error("链接解析失败");
|
if (!match) throw new Error("链接解析失败");
|
||||||
|
|
||||||
const parsedCode = drive.parseShareCode(match);
|
const parsedCode = drive.parseShareCode(match);
|
||||||
try {
|
|
||||||
let shareInfo = await drive.api.getShareInfo(parsedCode);
|
let shareInfo = await drive.api.getShareInfo(parsedCode);
|
||||||
if (shareInfo?.data) {
|
if (shareInfo) {
|
||||||
|
if (Array.isArray(shareInfo)) {
|
||||||
|
shareInfo = {
|
||||||
|
list: shareInfo,
|
||||||
|
...parsedCode,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
shareInfo = {
|
shareInfo = {
|
||||||
...shareInfo,
|
...shareInfo,
|
||||||
data: {
|
...parsedCode,
|
||||||
...shareInfo.data,
|
|
||||||
...parsedCode,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const params = drive.getSaveParams(shareInfo, folderId);
|
}
|
||||||
const result = await drive.api.saveFile(params);
|
const params = drive.getSaveParams(shareInfo, folderId);
|
||||||
|
const result = await drive.api.saveFile(params);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.code === 0) {
|
||||||
ElMessage.success(`${drive.name} 转存成功`);
|
ElMessage.success(`${drive.name} 转存成功`);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error);
|
ElMessage.error(result.message);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error instanceof Error ? error.message : `${drive.name} 转存失败`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -177,17 +202,33 @@ export const useResourceStore = defineStore("resource", {
|
|||||||
if (!match) throw new Error("链接解析失败");
|
if (!match) throw new Error("链接解析失败");
|
||||||
|
|
||||||
const parsedCode = matchedDrive.parseShareCode(match);
|
const parsedCode = matchedDrive.parseShareCode(match);
|
||||||
const shareInfo = await matchedDrive.api.getShareInfo(parsedCode);
|
let shareInfo = await matchedDrive.api.getShareInfo(parsedCode);
|
||||||
|
if (Array.isArray(shareInfo)) {
|
||||||
|
shareInfo = {
|
||||||
|
list: shareInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (shareInfo?.data?.list?.length) {
|
if (shareInfo?.list?.length) {
|
||||||
this.resources = [
|
this.resources = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "",
|
||||||
title: shareInfo.data.list.map((item) => item.fileName).join(", "),
|
channelInfo: {
|
||||||
cloudLinks: [url],
|
name: "自定义搜索",
|
||||||
cloudType: matchedDrive.type,
|
channelLogo: "",
|
||||||
channel: matchedDrive.name,
|
channelId: "",
|
||||||
pubDate: "",
|
},
|
||||||
|
displayList: true,
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: shareInfo.list.map((item) => item.fileName).join(", "),
|
||||||
|
cloudLinks: [url],
|
||||||
|
cloudType: matchedDrive.type,
|
||||||
|
channel: matchedDrive.name,
|
||||||
|
pubDate: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
42
frontend/src/stores/userSetting.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { UserSettingStore } from "@/types/user";
|
||||||
|
import { settingApi } from "@/api/setting";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
export const useUserSettingStore = defineStore("user", {
|
||||||
|
state: (): UserSettingStore => ({
|
||||||
|
globalSetting: null,
|
||||||
|
userSettings: {
|
||||||
|
cloud115Cookie: "",
|
||||||
|
quarkCookie: "",
|
||||||
|
},
|
||||||
|
displayStyle: "card",
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getSettings() {
|
||||||
|
const { data } = await settingApi.getSetting();
|
||||||
|
if (data) {
|
||||||
|
this.globalSetting = data.globalSetting;
|
||||||
|
this.userSettings = data.userSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveSettings() {
|
||||||
|
if (this.userSettings) {
|
||||||
|
const setting: UserSettingStore = {
|
||||||
|
userSettings: this.userSettings,
|
||||||
|
};
|
||||||
|
if (this.globalSetting) setting.globalSetting = this.globalSetting;
|
||||||
|
const res = await settingApi.saveSetting(setting);
|
||||||
|
if (res) {
|
||||||
|
this.getSettings();
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDisplayStyle(style: "table" | "card") {
|
||||||
|
this.displayStyle = style;
|
||||||
|
ElMessage.success(`切换成功,当前为${style}模式`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
11
frontend/src/styles/global.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
:root {
|
||||||
|
--theme-color: #3e3e3e;
|
||||||
|
--theme-theme: #133ab3;
|
||||||
|
--theme-background: #fafafa;
|
||||||
|
--theme-other_background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
18
frontend/src/types/douban.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export interface HotListParams {
|
||||||
|
type: string;
|
||||||
|
tag?: string;
|
||||||
|
page_limit?: string;
|
||||||
|
page_start?: string;
|
||||||
|
}
|
||||||
|
export interface HotListItem {
|
||||||
|
cover: string;
|
||||||
|
cover_x: number;
|
||||||
|
cover_y: number;
|
||||||
|
episodes_info: string;
|
||||||
|
id: string;
|
||||||
|
is_new: boolean;
|
||||||
|
playable: boolean;
|
||||||
|
rate: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
15
frontend/src/types/globals.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
declare global {
|
||||||
|
interface Location {
|
||||||
|
// 根据你的需求定义 location 的属性和方法
|
||||||
|
pathname: string;
|
||||||
|
search: string;
|
||||||
|
hash: string;
|
||||||
|
host: string;
|
||||||
|
// 其他属性和方法...
|
||||||
|
}
|
||||||
|
interface Window {
|
||||||
|
location: Location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -1,14 +1,28 @@
|
|||||||
export interface Resource {
|
export interface ResourceItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
|
image?: string;
|
||||||
cloudLinks: string[];
|
cloudLinks: string[];
|
||||||
|
tags?: string[];
|
||||||
|
content?: string;
|
||||||
pubDate: string;
|
pubDate: string;
|
||||||
cloudType: string;
|
cloudType: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
list: ResourceItem[];
|
||||||
|
displayList?: boolean;
|
||||||
|
channelInfo: {
|
||||||
|
channelId: string;
|
||||||
|
name: string;
|
||||||
|
channelLogo: string;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShareInfo {
|
export interface ShareInfo {
|
||||||
fileId: string;
|
fileId: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
@@ -17,13 +31,11 @@ export interface ShareInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareInfoResponse {
|
export interface ShareInfoResponse {
|
||||||
data: {
|
list: ShareInfo[];
|
||||||
list: ShareInfo[];
|
pwdId?: string;
|
||||||
pwdId?: string;
|
stoken?: string;
|
||||||
stoken?: string;
|
shareCode?: string;
|
||||||
shareCode?: string;
|
receiveCode?: string;
|
||||||
receiveCode?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Folder {
|
export interface Folder {
|
||||||
@@ -61,3 +73,23 @@ export interface SaveQuarkFileParams {
|
|||||||
pdir_fid: string;
|
pdir_fid: string;
|
||||||
scene: string;
|
scene: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TagColor {
|
||||||
|
baiduPan: string;
|
||||||
|
weiyun: string;
|
||||||
|
aliyun: string;
|
||||||
|
pan115: string;
|
||||||
|
quark: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GlobalSettingAttributes {
|
||||||
|
httpProxyHost: string;
|
||||||
|
httpProxyPort: number | string;
|
||||||
|
isProxyEnabled: boolean;
|
||||||
|
AdminUserCode: number;
|
||||||
|
CommonUserCode: number;
|
||||||
|
}
|
||||||
|
export interface UserSettingAttributes {
|
||||||
|
cloud115Cookie: string;
|
||||||
|
quarkCookie: string;
|
||||||
|
}
|
||||||
|
|||||||
15
frontend/src/types/response.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type RequestErrorCode = -1 | 400 | 401 | 402 | 403 | 500 | 501;
|
||||||
|
|
||||||
|
export interface RequestSuccess<T> {
|
||||||
|
code: 0;
|
||||||
|
data: T;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestError<T> {
|
||||||
|
code: RequestErrorCode;
|
||||||
|
message: string;
|
||||||
|
data?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RequestResult<T> = RequestSuccess<T> | RequestError<T>;
|
||||||
6
frontend/src/types/user.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { GlobalSettingAttributes, UserSettingAttributes } from "@/types";
|
||||||
|
export interface UserSettingStore {
|
||||||
|
globalSetting?: GlobalSettingAttributes | null;
|
||||||
|
userSettings: UserSettingAttributes;
|
||||||
|
displayStyle?: "table" | "card";
|
||||||
|
}
|
||||||
@@ -1,28 +1,63 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import { RequestResult } from "../types/response";
|
||||||
|
|
||||||
const request = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL as string,
|
baseURL: import.meta.env.VITE_API_BASE_URL as string,
|
||||||
timeout: 60000,
|
timeout: 9000,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
request.interceptors.response.use(
|
function isLoginAndRedirect(url: string) {
|
||||||
(response: AxiosResponse) => {
|
return url.includes("/api/user/login") || url.includes("/api/user/register");
|
||||||
const res = response.data;
|
}
|
||||||
if (!res.success) {
|
|
||||||
ElMessage.error(res.error || "请求失败");
|
axiosInstance.interceptors.request.use(
|
||||||
return Promise.reject(new Error(res.error || "请求失败"));
|
(config) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
} else if (!isLoginAndRedirect(config.url || "")) {
|
||||||
|
ElMessage.error("请先登录");
|
||||||
|
window.location.href = "/login";
|
||||||
}
|
}
|
||||||
return res;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
ElMessage.error(error.message || "网络错误");
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const res = response.data;
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
ElMessage.error("登录过期,请重新登录");
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.href = "/login";
|
||||||
|
return Promise.reject(new Error("登录过期,请重新登录"));
|
||||||
|
}
|
||||||
|
ElMessage.error(error.response.statusText);
|
||||||
|
return Promise.reject(new Error(error.response.statusText));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
get: <T>(
|
||||||
|
url: string,
|
||||||
|
config?: Record<string, any>
|
||||||
|
): Promise<RequestResult<T>> => {
|
||||||
|
return axiosInstance.get(url, { ...config });
|
||||||
|
},
|
||||||
|
post: axiosInstance.post,
|
||||||
|
put: axiosInstance.put,
|
||||||
|
delete: axiosInstance.delete,
|
||||||
|
};
|
||||||
|
|
||||||
export default request;
|
export default request;
|
||||||
|
|||||||
149
frontend/src/views/Douban.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="movie-wall">
|
||||||
|
<div v-for="movie in doubanStore.hotList" :key="movie.id" class="movie-item">
|
||||||
|
<div class="movie-poster">
|
||||||
|
<el-image
|
||||||
|
class="movie-poster-img"
|
||||||
|
:src="movie.cover"
|
||||||
|
fit="cover"
|
||||||
|
lazy
|
||||||
|
:alt="movie.title"
|
||||||
|
hide-on-click-modal
|
||||||
|
:preview-src-list="[movie.cover]"
|
||||||
|
/>
|
||||||
|
<div class="movie-rate">
|
||||||
|
{{ movie.rate }}
|
||||||
|
</div>
|
||||||
|
<div class="movie-poster-hover" @click="searchMovie(movie.title)">
|
||||||
|
<div class="movie-search">
|
||||||
|
<el-icon class="search_icon" size="28px"><Search /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="movie-info">
|
||||||
|
<el-link :href="movie.url" target="_blank" :underline="false" class="movie-title">{{
|
||||||
|
movie.title
|
||||||
|
}}</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { useDoubanStore } from "@/stores/douban";
|
||||||
|
interface CurrentParams {
|
||||||
|
type: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const routeParams = computed(
|
||||||
|
(): CurrentParams => ({ ...route.query }) as unknown as CurrentParams
|
||||||
|
);
|
||||||
|
const doubanStore = useDoubanStore();
|
||||||
|
if (routeParams.value) {
|
||||||
|
doubanStore.setCurrentParams(routeParams.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => routeParams.value,
|
||||||
|
() => {
|
||||||
|
console.log(routeParams.value);
|
||||||
|
doubanStore.setCurrentParams(routeParams.value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchMovie = (title: string) => {
|
||||||
|
router.push({ path: "/", query: { keyword: title } });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.movie-wall {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 200px);
|
||||||
|
grid-row-gap: 15px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-item {
|
||||||
|
width: 200px; /* 设置固定宽度 */
|
||||||
|
overflow: hidden; /* 确保内容不会超出卡片 */
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f9f9f9; /* 可选:设置背景颜色 */
|
||||||
|
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); /* 可选:设置阴影效果 */
|
||||||
|
border-radius: 15px; /* 设置图片圆角 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
position: relative;
|
||||||
|
padding: 15px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-poster-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
object-fit: cover; /* 确保图片使用cover模式 */
|
||||||
|
border-radius: 15px; /* 设置图片圆角 */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.movie-info {
|
||||||
|
/* margin-top: 8px; */
|
||||||
|
.movie-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.movie-poster {
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-poster-hover {
|
||||||
|
opacity: 0; /* 默认情况下隐藏 */
|
||||||
|
transition: opacity 0.3s ease; /* 添加过渡效果 */
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
/* height: 100%; */
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-poster:hover .movie-poster-hover {
|
||||||
|
opacity: 1; /* 鼠标移入时显示 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-rate {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: rgba(88, 83, 250, 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 0px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.movie-search {
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
93
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home" v-loading="resourcStore.loading" element-loading-background="rgba(0,0,0,0.6)">
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px"><aside-menu /></el-aside>
|
||||||
|
<el-container class="home-main">
|
||||||
|
<el-header :class="{ 'home-header': true, 'search-bar-active': !store.scrollTop }">
|
||||||
|
<search-bar />
|
||||||
|
</el-header>
|
||||||
|
<el-main class="home-main-main">
|
||||||
|
<router-view />
|
||||||
|
</el-main>
|
||||||
|
<!-- <el-aside class="home-aside"></el-aside> -->
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
<el-backtop :bottom="100">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
color: #1989fa;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
UP
|
||||||
|
</div>
|
||||||
|
</el-backtop>
|
||||||
|
</div>
|
||||||
|
<!-- <login v-else /> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useResourceStore } from "@/stores/resource";
|
||||||
|
import { useStore } from "@/stores/index";
|
||||||
|
import { useUserSettingStore } from "@/stores/userSetting";
|
||||||
|
import { onUnmounted } from "vue";
|
||||||
|
import AsideMenu from "@/components/AsideMenu.vue";
|
||||||
|
import SearchBar from "@/components/SearchBar.vue";
|
||||||
|
|
||||||
|
const resourcStore = useResourceStore();
|
||||||
|
const store = useStore();
|
||||||
|
const settingStore = useUserSettingStore();
|
||||||
|
settingStore.getSettings();
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = window.scrollY;
|
||||||
|
if (scrollTop > 50) {
|
||||||
|
store.scrollTop && store.setScrollTop(false);
|
||||||
|
} else {
|
||||||
|
!store.scrollTop && store.setScrollTop(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.home {
|
||||||
|
// padding: 20px;
|
||||||
|
min-width: 1000px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.home-header {
|
||||||
|
height: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
// padding: 0;
|
||||||
|
background-color: rgba(231, 235, 239, 0.7) !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
// background-color: var(--theme-other_background);
|
||||||
|
// box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
|
||||||
|
// border-radius: 20px;
|
||||||
|
}
|
||||||
|
.home-main {
|
||||||
|
width: 1000px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.home-main-main {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
.home-aside {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<search-bar />
|
|
||||||
<resource-list />
|
|
||||||
<el-backtop :bottom="100">
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--el-bg-color-overlay);
|
|
||||||
box-shadow: var(--el-box-shadow-lighter);
|
|
||||||
text-align: center;
|
|
||||||
line-height: 40px;
|
|
||||||
color: #1989fa;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
UP
|
|
||||||
</div>
|
|
||||||
</el-backtop>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import SearchBar from "@/components/SearchBar.vue";
|
|
||||||
import ResourceList from "@/components/ResourceList.vue";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.home {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
259
frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-register">
|
||||||
|
<div class="login-bg"></div>
|
||||||
|
<el-card class="card">
|
||||||
|
<!-- 登录与注册切换 -->
|
||||||
|
<el-tabs v-model="activeTab" class="tabs">
|
||||||
|
<el-tab-pane label="登录" name="login"></el-tab-pane>
|
||||||
|
<el-tab-pane label="注册" name="register"> </el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
|
<el-form
|
||||||
|
v-if="activeTab === 'login'"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
ref="loginFormRef"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item prop="username" class="form-item">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="用户名"
|
||||||
|
name="username"
|
||||||
|
autocomplete="on"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password" class="form-item">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
class="form-input"
|
||||||
|
show-password="true"
|
||||||
|
autocomplete="on"
|
||||||
|
name="password"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" class="form-submit" @click="loginFormRefValidate">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 注册表单 -->
|
||||||
|
<el-form
|
||||||
|
v-if="activeTab === 'register'"
|
||||||
|
:model="registerForm"
|
||||||
|
:rules="registerRules"
|
||||||
|
ref="registerFormRef"
|
||||||
|
label-position="top"
|
||||||
|
><el-form-item prop="username" class="form-item">
|
||||||
|
<el-input v-model="registerForm.username" placeholder="用户名" class="form-input" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password" class="form-item">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password" class="form-item">
|
||||||
|
<el-input
|
||||||
|
v-model="password2"
|
||||||
|
type="password"
|
||||||
|
placeholder="再次输入密码"
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="registerCode" class="form-item">
|
||||||
|
<el-input v-model="registerForm.registerCode" placeholder="注册码" class="form-input" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" class="form-submit" @click="handleRegister"> 注册 </el-button>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup type="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { userApi } from "@/api/user";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
const activeTab = ref("login"); // 默认显示登录表单
|
||||||
|
|
||||||
|
const loginForm = ref({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const registerForm = ref({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
registerCode: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const password2 = ref("");
|
||||||
|
|
||||||
|
const loginRules = {
|
||||||
|
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||||
|
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerRules = {
|
||||||
|
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||||
|
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||||
|
registerCode: [{ required: true, message: "请输入注册码", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginFormRef = ref(null);
|
||||||
|
const registerFormRef = ref(null);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
const res = await userApi.login(loginForm.value);
|
||||||
|
if (res.code === 0) {
|
||||||
|
const { token } = res.data;
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
// 路由跳转首页
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("登录失败", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const loginFormRefValidate = () => {
|
||||||
|
loginFormRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
handleLogin();
|
||||||
|
} else {
|
||||||
|
ElMessage.error("登录表单验证失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
registerFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if(password2.value !== registerForm.value.password){
|
||||||
|
return ElMessage.error("两次输入的密码不一致");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await userApi.register(registerForm.value);
|
||||||
|
if (res.code === 0) {
|
||||||
|
ElMessage.success("注册成功");
|
||||||
|
loginForm.value.username = registerForm.value.username
|
||||||
|
loginForm.value.password = registerForm.value.password
|
||||||
|
handleLogin()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "注册失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || "注册失败");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("注册表单验证失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login-register {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url("../assets/images/login-bg.jpg");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
-webkit-filter: blur(3px);
|
||||||
|
-moz-filter: blur(3px);
|
||||||
|
-o-filter: blur(3px);
|
||||||
|
-ms-filter: blur(3px);
|
||||||
|
filter: blur(3px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
width: 480px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0px 20px 60px rgba(123, 61, 224, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #6366f1;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-login {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-agreement {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
frontend/src/views/ResourceList.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-list">
|
||||||
|
<div :class="{ 'resource-list__header': true }">
|
||||||
|
<div class="header_left">
|
||||||
|
<div class="refresh_btn" @click="refreshResources">
|
||||||
|
<el-icon class="type_icon" size="20px"><Refresh /></el-icon>最新资源<span
|
||||||
|
class="item-count"
|
||||||
|
>(上次刷新时间:{{ resourceStore.lastUpdateTime }})</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header_right">
|
||||||
|
<el-icon
|
||||||
|
class="type_icon"
|
||||||
|
v-if="userStore.displayStyle === 'card'"
|
||||||
|
@click="setDisplayStyle('table')"
|
||||||
|
><Menu
|
||||||
|
/></el-icon>
|
||||||
|
<el-icon v-else class="type_icon" @click="setDisplayStyle('card')"><Fold /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ResourceTable
|
||||||
|
@loadMore="handleLoadMore"
|
||||||
|
@searchMovieforTag="searchMovieforTag"
|
||||||
|
@save="handleSave"
|
||||||
|
v-if="userStore.displayStyle === 'table'"
|
||||||
|
></ResourceTable>
|
||||||
|
<ResourceCard
|
||||||
|
@loadMore="handleLoadMore"
|
||||||
|
@searchMovieforTag="searchMovieforTag"
|
||||||
|
@save="handleSave"
|
||||||
|
v-else
|
||||||
|
></ResourceCard>
|
||||||
|
<el-empty v-if="resourceStore.resources.length === 0" :image-size="200" />
|
||||||
|
<el-dialog v-model="folderDialogVisible" title="选择保存目录" v-if="currentResource">
|
||||||
|
<template #header="{ titleId }">
|
||||||
|
<div class="my-header">
|
||||||
|
<div :id="titleId">
|
||||||
|
<el-tag
|
||||||
|
:type="resourceStore.tagColor[currentResource.cloudType as keyof TagColor]"
|
||||||
|
effect="dark"
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{{ currentResource.cloudType }}
|
||||||
|
</el-tag>
|
||||||
|
选择保存目录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<folder-select
|
||||||
|
v-if="folderDialogVisible"
|
||||||
|
@select="handleFolderSelect"
|
||||||
|
@close="folderDialogVisible = false"
|
||||||
|
:cloudType="currentResource.cloudType"
|
||||||
|
/>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="folderDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveBtnClick">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useResourceStore } from "@/stores/resource";
|
||||||
|
import { useUserSettingStore } from "@/stores/userSetting";
|
||||||
|
import FolderSelect from "@/components/Home/FolderSelect.vue";
|
||||||
|
import ResourceTable from "@/components/Home/ResourceTable.vue";
|
||||||
|
import type { ResourceItem, TagColor } from "@/types";
|
||||||
|
import ResourceCard from "@/components/Home/ResourceCard.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const resourceStore = useResourceStore();
|
||||||
|
const userStore = useUserSettingStore();
|
||||||
|
const folderDialogVisible = ref(false);
|
||||||
|
const currentResource = ref<ResourceItem | null>(null);
|
||||||
|
const currentFolderId = ref<string | null>(null);
|
||||||
|
|
||||||
|
const refreshResources = async () => {
|
||||||
|
resourceStore.searchResources("", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = (resource: ResourceItem) => {
|
||||||
|
currentResource.value = resource;
|
||||||
|
folderDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFolderSelect = async (folderId: string) => {
|
||||||
|
if (!currentResource.value) return;
|
||||||
|
currentFolderId.value = folderId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (!currentResource.value || !currentFolderId.value) return;
|
||||||
|
folderDialogVisible.value = false;
|
||||||
|
await resourceStore.saveResource(currentResource.value, currentFolderId.value);
|
||||||
|
};
|
||||||
|
const setDisplayStyle = (style: string) => {
|
||||||
|
console.log(userStore);
|
||||||
|
userStore.setDisplayStyle(style as "card" | "table");
|
||||||
|
};
|
||||||
|
// 添加加载更多处理函数
|
||||||
|
const handleLoadMore = (channelId: string) => {
|
||||||
|
resourceStore.searchResources("", true, channelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMovieforTag = (tag: string) => {
|
||||||
|
console.log("iiii");
|
||||||
|
router.push({ path: "/", query: { keyword: tag } });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.resource-list {
|
||||||
|
/* margin-top: 20px; */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.resource-list__header {
|
||||||
|
height: 48px;
|
||||||
|
background-color: var(--theme-other_background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
.header_right {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.type_icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
size: 48px;
|
||||||
|
}
|
||||||
|
.refresh_btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
.item-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expand-column) {
|
||||||
|
.cell {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expanded-cell) {
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__expand-icon) {
|
||||||
|
height: 23px;
|
||||||
|
line-height: 23px;
|
||||||
|
}
|
||||||
|
.load-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
frontend/src/views/Setting.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<el-card class="setting-card" v-if="settingStore.globalSetting">
|
||||||
|
<h2>网络配置</h2>
|
||||||
|
<div class="section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="proxyDomain">代理域名:</label>
|
||||||
|
<el-input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="proxyDomain"
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
v-model="settingStore.globalSetting.httpProxyHost"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="proxyPort">代理端口:</label>
|
||||||
|
<el-input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="proxyPort"
|
||||||
|
placeholder="7890"
|
||||||
|
v-model="settingStore.globalSetting.httpProxyPort"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="AdminUserCode">管理员注册码:</label>
|
||||||
|
<el-input-number
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="AdminUserCode"
|
||||||
|
:controls="false"
|
||||||
|
:precision="0"
|
||||||
|
placeholder="设置管理员注册码"
|
||||||
|
v-model="settingStore.globalSetting.AdminUserCode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="CommonUserCode">普通用户注册码:</label>
|
||||||
|
<el-input-number
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
id="CommonUserCode"
|
||||||
|
placeholder="设置普通用户注册码"
|
||||||
|
v-model="settingStore.globalSetting.CommonUserCode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="isProxyEnabled">启用代理:</label>
|
||||||
|
<el-switch v-model="settingStore.globalSetting.isProxyEnabled" @change="saveSettings" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="setting-card">
|
||||||
|
<h2>用户配置</h2>
|
||||||
|
<div class="section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookie115">115网盘Cookie:</label>
|
||||||
|
<el-input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="cookie115"
|
||||||
|
v-model="settingStore.userSettings.cloud115Cookie"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cookieQuark">夸克网盘Cookie:</label>
|
||||||
|
<el-input
|
||||||
|
class="form-input"
|
||||||
|
type="text"
|
||||||
|
id="cookieQuark"
|
||||||
|
v-model="settingStore.userSettings.quarkCookie"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-setting-tips">
|
||||||
|
<h3>帮助</h3>
|
||||||
|
<div>
|
||||||
|
<el-link
|
||||||
|
target="_blank"
|
||||||
|
href="https://alist.nn.ci/zh/guide/drivers/115.html#cookie%E8%8E%B7%E5%8F%96%E6%96%B9%E5%BC%8F"
|
||||||
|
>如何获取115网盘cookie?</el-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-link target="_blank" href="https://alist.nn.ci/zh/guide/drivers/quark.html#cookie"
|
||||||
|
>如何获取夸克网盘cookie?</el-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-button @click="saveSettings">保存设置</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserSettingStore } from "@/stores/userSetting";
|
||||||
|
const settingStore = useUserSettingStore();
|
||||||
|
settingStore.getSettings();
|
||||||
|
|
||||||
|
const saveSettings = () => {
|
||||||
|
settingStore.saveSettings();
|
||||||
|
// Add your save logic here
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settings {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.setting-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
::v-deep .el-input__inner {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"typeRoots": ["./node_modules/@types", "./types"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ export default defineConfig({
|
|||||||
resolvers: [ElementPlusResolver()],
|
resolvers: [ElementPlusResolver()],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `@use "@/styles/global.scss";`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
@@ -41,6 +48,10 @@ export default defineConfig({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/tele-images": {
|
||||||
|
target: process.env.VITE_API_BASE_URL_PROXY || "http://127.0.0.1:8009",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
35
nginx.conf
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# nginx.conf
|
||||||
|
user nginx; # 定义 Nginx 进程的运行用户
|
||||||
|
worker_processes 1; # 设置 Nginx 进程数
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024; # 每个 worker 进程最大连接数
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
server {
|
||||||
|
listen 8008;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
rewrite ^/api/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://localhost:8009;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /tele-images/ {
|
||||||
|
proxy_pass http://localhost:8009;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloud-saver",
|
"name": "cloud-saver",
|
||||||
"version": "0.0.8",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"frontend",
|
"frontend",
|
||||||
@@ -22,11 +22,19 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"rimraf": "^5.0.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier": "^3.2.5"
|
"rimraf": "^5.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=6.0.0"
|
"pnpm": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"bcrypt",
|
||||||
|
"esbuild",
|
||||||
|
"sqlite3",
|
||||||
|
"vue-demi"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||