Repository: jiangrui1994/CloudSaver
Branch: main
Commit: f67a6b274a40
Files: 125
Total size: 272.7 KB
Directory structure:
gitextract_ls8v8cv6/
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── docker-build-test.yml
│ └── docker-image.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── Dockerfile
├── LICENSE
├── README.md
├── backend/
│ ├── .env.example
│ ├── package.json
│ ├── src/
│ │ ├── app.ts
│ │ ├── config/
│ │ │ ├── database.ts
│ │ │ └── index.ts
│ │ ├── controllers/
│ │ │ ├── BaseCloudController.ts
│ │ │ ├── BaseController.ts
│ │ │ ├── cloud115.ts
│ │ │ ├── douban.ts
│ │ │ ├── quark.ts
│ │ │ ├── resource.ts
│ │ │ ├── setting.ts
│ │ │ ├── sponsors.ts
│ │ │ ├── teleImages.ts
│ │ │ └── user.ts
│ │ ├── core/
│ │ │ ├── ApiResponse.ts
│ │ │ └── types.ts
│ │ ├── inversify.config.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts
│ │ │ ├── cors.ts
│ │ │ ├── errorHandler.ts
│ │ │ ├── index.ts
│ │ │ ├── rateLimiter.ts
│ │ │ ├── requestLogger.ts
│ │ │ └── validateRequest.ts
│ │ ├── models/
│ │ │ ├── GlobalSetting.ts
│ │ │ ├── User.ts
│ │ │ └── UserSetting.ts
│ │ ├── routes/
│ │ │ └── api.ts
│ │ ├── services/
│ │ │ ├── Cloud115Service.ts
│ │ │ ├── DatabaseService.ts
│ │ │ ├── DoubanService.ts
│ │ │ ├── ImageService.ts
│ │ │ ├── QuarkService.ts
│ │ │ ├── Searcher.ts
│ │ │ ├── SettingService.ts
│ │ │ ├── SponsorsService.ts
│ │ │ └── UserService.ts
│ │ ├── sponsors/
│ │ │ └── sponsors.json
│ │ ├── types/
│ │ │ ├── cloud.ts
│ │ │ ├── cloud115.ts
│ │ │ ├── express.ts
│ │ │ ├── index.ts
│ │ │ └── services.ts
│ │ └── utils/
│ │ ├── axiosInstance.ts
│ │ ├── handleError.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── response.ts
│ │ └── responseHandler.ts
│ └── tsconfig.json
├── docker-entrypoint.sh
├── frontend/
│ ├── .env
│ ├── auto-imports.d.ts
│ ├── components.d.ts
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── src/
│ │ ├── App.vue
│ │ ├── api/
│ │ │ ├── cloud115.ts
│ │ │ ├── douban.ts
│ │ │ ├── quark.ts
│ │ │ ├── resource.ts
│ │ │ ├── setting.ts
│ │ │ └── user.ts
│ │ ├── components/
│ │ │ ├── AsideMenu.vue
│ │ │ ├── Home/
│ │ │ │ ├── FolderSelect.vue
│ │ │ │ ├── ResourceCard.vue
│ │ │ │ ├── ResourceSelect.vue
│ │ │ │ └── ResourceTable.vue
│ │ │ ├── SearchBar.vue
│ │ │ └── mobile/
│ │ │ ├── FolderSelect.vue
│ │ │ ├── ResourceCard.vue
│ │ │ └── ResourceSelect.vue
│ │ ├── constants/
│ │ │ ├── project.ts
│ │ │ └── storage.ts
│ │ ├── env.d.ts
│ │ ├── main.ts
│ │ ├── router/
│ │ │ ├── index.ts
│ │ │ ├── mobile-routes.ts
│ │ │ └── pc-routes.ts
│ │ ├── stores/
│ │ │ ├── douban.ts
│ │ │ ├── index.ts
│ │ │ ├── resource.ts
│ │ │ └── userSetting.ts
│ │ ├── styles/
│ │ │ ├── common.scss
│ │ │ ├── global.scss
│ │ │ ├── mobile.scss
│ │ │ └── responsive.scss
│ │ ├── types/
│ │ │ ├── douban.ts
│ │ │ ├── globals.d.ts
│ │ │ ├── index.ts
│ │ │ ├── response.ts
│ │ │ └── user.ts
│ │ ├── utils/
│ │ │ ├── device.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ └── request.ts
│ │ └── views/
│ │ ├── Douban.vue
│ │ ├── Home.vue
│ │ ├── ResourceList.vue
│ │ ├── Setting.vue
│ │ ├── Thanks.vue
│ │ ├── mobile/
│ │ │ ├── Douban.vue
│ │ │ ├── Home.vue
│ │ │ ├── Login.vue
│ │ │ ├── ResourceList.vue
│ │ │ └── Setting.vue
│ │ └── pc/
│ │ └── Login.vue
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── nginx.conf
├── package.json
└── pnpm-workspace.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
node_modules
dist
build
coverage
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
ignorePatterns: ["node_modules", "dist", "build", "coverage"],
env: {
node: true,
es6: true,
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/explicit-function-return-type": 0,
},
overrides: [
{
files: ["frontend/**/*.{js,ts,vue}"],
env: {
browser: true,
},
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended",
"plugin:prettier/recommended",
],
plugins: ["@typescript-eslint", "vue"],
rules: {
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off",
"vue/no-v-html": "off",
},
},
{
files: ["backend/**/*.{js,ts}"],
env: {
node: true,
},
rules: {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-non-null-assertion": "warn",
},
},
],
};
================================================
FILE: .github/workflows/docker-build-test.yml
================================================
name: Build and Push Multi-Arch Docker Image for Test
on:
workflow_dispatch: # 添加手动触发
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 必须授权以推送镜像
env:
REPO_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
IMAGE_NAME: cloudsaver
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置小写镜像名称
run: |
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
- name: 登录到 GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 登录到 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 设置 QEMU 支持多架构
uses: docker/setup-qemu-action@v2
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 构建并推送多架构 Docker 镜像
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64 # 指定架构:x86_64 和 ARM64
push: true
tags: |
ghcr.io/${{ env.LOWER_NAME }}:test
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:test
================================================
FILE: .github/workflows/docker-image.yml
================================================
name: Docker Image CI/CD
on:
push:
tags: ["v*.*.*"] # 支持标签触发(如 v1.0.0)
workflow_dispatch: # 添加手动触发
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 必须授权以推送镜像
env:
REPO_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
IMAGE_NAME: cloudsaver
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置小写镜像名称和版本
run: |
LOWER_NAME=$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')
echo "LOWER_NAME=$LOWER_NAME" >> $GITHUB_ENV
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: 登录到 GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 登录到 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 设置 QEMU 支持多架构
uses: docker/setup-qemu-action@v2
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 构建并推送 Docker 镜像
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64 # 指定架构:x86_64 和 ARM64
push: true
tags: |
ghcr.io/${{ env.LOWER_NAME }}:latest
ghcr.io/${{ env.LOWER_NAME }}:${{ env.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
================================================
FILE: .gitignore
================================================
node_modules/
logs/
dist/
.env
.env.local
.env.*.local
*.tar
# 数据库数据
*.sqlite
# 保留模板
!.env.example
!frontend/.env
# 其他敏感文件
config.private.ts
*.pem
*.key
.DS_Store
*.log
================================================
FILE: .prettierignore
================================================
# 构建产物
dist
build
coverage
# 依赖目录
node_modules
# 日志文件
*.log
# 环境配置
.env*
!.env.example
# 编辑器配置
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 系统文件
.DS_Store
Thumbs.db
# 版本控制
.git
================================================
FILE: .prettierrc.js
================================================
module.exports = {
semi: true,
trailingComma: "es5",
singleQuote: false,
printWidth: 100,
tabWidth: 2,
useTabs: false,
endOfLine: "auto",
arrowParens: "always",
bracketSpacing: true,
embeddedLanguageFormatting: "auto",
htmlWhitespaceSensitivity: "css",
vueIndentScriptAndStyle: false,
};
================================================
FILE: Dockerfile
================================================
# 构建前端项目
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
# 创建配置和数据目录
RUN mkdir -p /app/config /app/data
# 复制前端构建产物到 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
# 设置数据卷
VOLUME ["/app/config", "/app/data"]
# 暴露端口
EXPOSE 8008
# 启动脚本
COPY docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh
# 启动服务
ENTRYPOINT ["/app/docker-entrypoint.sh"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 CloudSaver
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# CloudSaver



[](https://github.com/jiangrui1994/CloudSaver/stargazers)

一个基于 Vue 3 + Express 的网盘资源搜索与转存工具,支持响应式布局,移动端与PC完美适配,可通过 Docker 一键部署。
官方Telegram群组:[https://t.me/+4fWSSbQn5rMxYjM1](https://t.me/+4fWSSbQn5rMxYjM1)
官方QQ交流群([群地址](https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l#lsPla))
版本更新日志:[https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)
CloudSaver部署与使用常见问题(包含更多搜索频道):[https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l](https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l)
密码 me16 点个Star呗~
⚠️关于项目更新与需求处理的核心声明:[https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx](https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx)
---
⚠️**由于某些原因,[新版本](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)内容不包含在此开源仓库(停留在V0.2.5版本),如需使用,请使用docker镜像进行部署使用。**
---
**🔒 重要安全提醒|关于本项目私有化部署的强制建议**
为保障您的数据安全与隐私权益,请务必**通过Docker自行私有化部署本项目**。我们**强烈反对**使用任何第三方提供的在线网站或他人部署的服务,原因如下:
⚠️ **高风险预警**
- 本项目涉及**网盘Cookie等敏感凭据**,若使用他人服务:
▶ 您的账号密码、隐私文件可能遭恶意窃取或篡改
▶ 攻击者可利用Cookie直接登录您的网盘实施破坏
▶ 数据泄露、资产损失等后果需完全由使用者自行承担
🚫 **严正声明**
1. 本项目**从未且不会**提供任何形式的在线服务、公开Demo或托管平台
2. **任何声称与本项目相关的在线网站均为未授权第三方搭建**,存在蓄意作恶的高风险
3. 如因使用非自建服务导致损失,本项目开发者**不承担任何法律责任**
❓ **常见问题**
Q: 是否有在线Demo可直接试用?
A: **绝无可能!** 任何在线服务都与本项目无关,请立即关闭避免信息泄露
Q: 为何不能使用他人部署好的服务?
A: Cookie等同于账号密码,交出Cookie=交出家门钥匙,请勿将身家安全托付陌生人
Q: 如何确保100%安全?
A: 唯一可信方案:通过官方仓库代码+自主服务器部署,全程数据闭环
---
**🛡️ 最后一次严肃提醒**
您的账号安全只应掌握在自己手中!
请立即执行私有化部署 ▶ 避免无法挽回的数据灾难
---
## 功能特性
- 🔍 多源资源搜索
- 支持多个资源订阅源搜索
- 支持关键词搜索与资源链接解析
- 支持豆瓣热门榜单展示
- 💾 网盘资源转存
- 支持**115 网盘,夸克网盘,天翼网盘,123云盘**一键转存
- 支持转存文件夹展示与选择
- 👥 多用户系统
- 支持用户注册登录
- 支持管理员与普通用户权限区分
- 📱 响应式设计
- 支持 PC 端与移动端自适应布局
- 针对不同设备优化的交互体验
## 产品展示
点击展开截图预览
### PC 端
登录页面/榜单
资源搜索/资源详情
资源转存
### 移动端
## 技术栈
### 前端
- 核心框架
- Vue 3
- TypeScript
- Vite
- 状态管理
- Pinia
- 路由管理
- Vue Router
- UI 组件库
- Element Plus (PC)
- Vant (Mobile)
- 工具库
- Axios
### 后端
- 运行环境
- Node.js
- Express
- 数据存储
- SQLite3
## 环境要求
- Node.js >= 18.x
- pnpm >= 8.x (推荐)
## 快速开始
### 开发环境
1. 克隆项目
```bash
git clone https://github.com/jiangrui1994/CloudSaver.git
cd CloudSaver
```
2. 安装依赖
```bash
pnpm install
```
3. 配置环境变量
```bash
cp ./backend/.env.example ./backend/.env
```
根据 `.env.example` 文件说明配置必要的环境变量。
4. 启动开发服务器
```bash
pnpm dev
```
### 生产环境部署
1. 构建前端
```bash
pnpm build:frontend
```
2. 构建后端
```bash
cd backend
pnpm build
```
3. 启动服务
```bash
pnpm start
```
### Docker 部署
说明:镜像源有**两个地址**供选择,下面部署命令中使用的是dockerhub托管的地址为例,github托管的地址请自行替换
- dockerhub托管:
- `jiangrui1994/cloudsaver:latest` 稳定版
- `jiangrui1994/cloudsaver:test` 测试版 (包含最新功能和bug修复,但可能不如稳定版稳定)
- github托管:
- `ghcr.io/jiangrui1994/cloudsaver:latest` 稳定版
- `ghcr.io/jiangrui1994/cloudsaver:test` 测试版 (包含最新功能和bug修复,但可能不如稳定版稳定)
#### 单容器部署
稳定版:
```bash
docker run -d \
-p 8008:8008 \
-v /your/local/path/data:/app/data \
-v /your/local/path/config:/app/config \
--name cloud-saver \
jiangrui1994/cloudsaver:latest
```
测试版(包含最新功能和bug修复,但可能不如稳定版稳定):
```bash
docker run -d \
-p 8008:8008 \
-v /your/local/path/data:/app/data \
-v /your/local/path/config:/app/config \
--name cloud-saver \
jiangrui1994/cloudsaver:test
```
#### Docker Compose 部署
创建 `docker-compose.yml` 文件:
稳定版:
```yaml
version: "3"
services:
cloudsaver:
image: jiangrui1994/cloudsaver:latest
container_name: cloud-saver
ports:
- "8008:8008"
volumes:
- /your/local/path/data:/app/data
- /your/local/path/config:/app/config
restart: unless-stopped
```
测试版:
```yaml
version: "3"
services:
cloudsaver:
image: jiangrui1994/cloudsaver:test
container_name: cloud-saver
ports:
- "8008:8008"
volumes:
- /your/local/path/data:/app/data
- /your/local/path/config:/app/config
restart: unless-stopped
```
#### /app/config 目录说明
- `env` 文件:包含后端环境变量配置
```bash
# JWT配置
JWT_SECRET=your_jwt_secret_here
# Telegram配置
TELEGRAM_BASE_URL=https://t.me/s
# Telegram频道配置(0.3.0及之后版本无效)
TELE_CHANNELS=[{"id":"xxxx","name":"xxxx资源分享"}]
```
运行:
```bash
docker-compose up -d
```
> **注意**: 测试版(:test标签)包含最新的功能开发和bug修复,但可能存在不稳定因素。建议生产环境使用稳定版(:latest标签)。
## 注意事项
1. 资源搜索需要配置代理环境
2. 默认注册码
- 管理员:230713
- 普通用户:9527
## 联系方式
官方QQ交流群(群地址(https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l#lsPla))
## 支持项目
如果您觉得这个项目对您有帮助,可以考虑给予一点支持,这将帮助我们持续改进项目 ❤️
您可以:
- ⭐ 给项目点个 Star
- 🎉 分享给更多有需要的朋友
- ☕ 请作者喝杯冰阔乐或咖啡
- 💰 **赞赏了一定记得和我联系**
## 特别声明
1. 本项目仅供学习交流使用,请勿用于非法用途
2. 仅支持个人使用,不支持任何形式的商业使用
3. 禁止在项目页面进行任何形式的广告宣传
4. 所有搜索到的资源均来自第三方,本项目不对其真实性、合法性做出任何保证
## 贡献指南
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 提交 Pull Request
## 开源协议
本项目基于 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解更多细节
## 鸣谢
- 👨💻 感谢所有为这个项目做出贡献的开发者们!
- 👥 感谢所有使用本项目并提供反馈的用户!
- 感谢所有给予支持和鼓励的朋友们!
## 赞助
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne
## Star History
[](https://www.star-history.com/#jiangrui1994/cloudsaver&Date)
================================================
FILE: backend/.env.example
================================================
# JWT配置
JWT_SECRET=your_jwt_secret_here
# Telegram配置
TELEGRAM_BASE_URL=https://t.me/s
# Telegram频道配置
TELE_CHANNELS=[]
================================================
FILE: backend/package.json
================================================
{
"name": "cloud-saver-server",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "nodemon --exec ts-node src/app.ts",
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"axios": "^1.6.7",
"bcrypt": "^5.1.1",
"cheerio": "^1.0.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"inversify": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"rss-parser": "^3.13.0",
"sequelize": "^6.37.5",
"socket.io": "^4.8.1",
"sqlite3": "^5.1.7",
"tunnel": "^0.0.6",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^20.11.25",
"@types/tunnel": "^0.0.7",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
}
}
================================================
FILE: backend/src/app.ts
================================================
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
import "./types/express";
import express from "express";
import { container } from "./inversify.config";
import { TYPES } from "./core/types";
import { DatabaseService } from "./services/DatabaseService";
import { setupMiddlewares } from "./middleware";
import routes from "./routes/api";
import { logger } from "./utils/logger";
import { errorHandler } from "./middleware/errorHandler";
class App {
private app = express();
private databaseService = container.get(TYPES.DatabaseService);
constructor() {
this.setupExpress();
}
private setupExpress(): void {
// 设置中间件
setupMiddlewares(this.app);
// 设置路由
this.app.use("/", routes);
this.app.use(errorHandler);
}
public async start(): Promise {
try {
// 初始化数据库
await this.databaseService.initialize();
logger.info("数据库初始化成功");
// 启动服务器
const port = process.env.PORT || 8009;
this.app.listen(port, () => {
logger.info(`
🚀 服务器启动成功
🌍 监听端口: ${port}
🔧 运行环境: ${process.env.NODE_ENV || "development"}
`);
});
} catch (error) {
logger.error("服务器启动失败:", error);
process.exit(1);
}
}
}
// 创建并启动应用
const application = new App();
application.start().catch((error) => {
logger.error("应用程序启动失败:", error);
process.exit(1);
});
export default application;
================================================
FILE: backend/src/config/database.ts
================================================
// backend/src/config/database.ts
import { Sequelize } from "sequelize";
const sequelize = new Sequelize({
dialect: "sqlite",
storage: "./data/database.sqlite",
});
export default sequelize;
================================================
FILE: backend/src/config/index.ts
================================================
import dotenv from "dotenv";
// 加载.env文件
dotenv.config();
interface Channel {
id: string;
name: string;
}
interface CloudPatterns {
baiduPan: RegExp;
tianyi: RegExp;
aliyun: RegExp;
pan115: RegExp;
pan123: RegExp;
quark: RegExp;
yidong: RegExp;
}
interface Config {
jwtSecret: string;
telegram: {
baseUrl: string;
channels: Channel[];
};
cloudPatterns: CloudPatterns;
app: {
port: number;
env: string;
};
database: {
type: string;
path: string;
};
jwt: {
secret: string;
expiresIn: string;
};
}
// 从环境变量读取频道配置
const getTeleChannels = (): Channel[] => {
try {
const channelsStr = process.env.TELE_CHANNELS;
if (channelsStr) {
return JSON.parse(channelsStr);
}
} catch (error) {
console.warn("无法解析 TELE_CHANNELS 环境变量,使用默认配置");
}
// 默认配置
return [];
};
export const config: Config = {
app: {
port: parseInt(process.env.PORT || "8009"),
env: process.env.NODE_ENV || "development",
},
database: {
type: "sqlite",
path: "./data/database.sqlite",
},
jwt: {
secret: process.env.JWT_SECRET || "your-secret-key",
expiresIn: "6h",
},
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
telegram: {
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
channels: getTeleChannels(),
},
cloudPatterns: {
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
aliyun: /https?:\/\/\w+\.(?:alipan|aliyundrive)\.com\/[^\s<>"]+/g,
// pan115有两个域名 115.com 和 anxia.com 和 115cdn.com
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
// 修改为匹配所有以123开头的域名
// eslint-disable-next-line no-useless-escape
pan123: /https?:\/\/(?:www\.)?123[^\/\s<>"]+\.com\/s\/[^\s<>"]+/g,
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
yidong: /https?:\/\/caiyun\.139\.com\/[^\s<>"]+/g,
},
};
================================================
FILE: backend/src/controllers/BaseCloudController.ts
================================================
import { Request, Response } from "express";
import { BaseController } from "./BaseController";
import { ICloudStorageService } from "@/types/services";
export abstract class BaseCloudController extends BaseController {
constructor(protected cloudService: ICloudStorageService) {
super();
}
async getShareInfo(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { shareCode, receiveCode } = req.query;
// await this.cloudService.setCookie(req);
return await this.cloudService.getShareInfo(shareCode as string, receiveCode as string);
});
}
async getFolderList(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { parentCid } = req.query;
await this.cloudService.setCookie(req);
return await this.cloudService.getFolderList(parentCid as string);
});
}
async saveFile(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
await this.cloudService.setCookie(req);
return await this.cloudService.saveSharedFile(req.body);
});
}
}
================================================
FILE: backend/src/controllers/BaseController.ts
================================================
import { Request, Response } from "express";
import { ApiResponse } from "../core/ApiResponse";
interface ApiResponseData {
data?: T;
message?: string;
}
export abstract class BaseController {
protected async handleRequest(
req: Request,
res: Response,
action: () => Promise | void>
): Promise {
try {
const result = await action();
if (result) {
res.json(ApiResponse.success(result.data, result.message));
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "未知错误";
res.status(200).json(ApiResponse.error(errorMessage));
}
}
}
================================================
FILE: backend/src/controllers/cloud115.ts
================================================
import { Cloud115Service } from "../services/Cloud115Service";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { BaseCloudController } from "./BaseCloudController";
@injectable()
export class Cloud115Controller extends BaseCloudController {
constructor(@inject(TYPES.Cloud115Service) cloud115Service: Cloud115Service) {
super(cloud115Service);
}
}
================================================
FILE: backend/src/controllers/douban.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { DoubanService } from "../services/DoubanService";
import { BaseController } from "./BaseController";
@injectable()
export class DoubanController extends BaseController {
constructor(@inject(TYPES.DoubanService) private doubanService: DoubanService) {
super();
}
async getDoubanHotList(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
const result = await this.doubanService.getHotList({
type: type as string,
tag: tag as string,
page_limit: page_limit as string,
page_start: page_start as string,
});
return result;
});
}
}
================================================
FILE: backend/src/controllers/quark.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { QuarkService } from "../services/QuarkService";
import { BaseCloudController } from "./BaseCloudController";
@injectable()
export class QuarkController extends BaseCloudController {
constructor(@inject(TYPES.QuarkService) quarkService: QuarkService) {
super(quarkService);
}
}
================================================
FILE: backend/src/controllers/resource.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { Searcher } from "../services/Searcher";
import { BaseController } from "./BaseController";
@injectable()
export class ResourceController extends BaseController {
constructor(@inject(TYPES.Searcher) private searcher: Searcher) {
super();
}
async search(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { keyword, channelId = "", lastMessageId = "" } = req.query;
return await this.searcher.searchAll(
keyword as string,
channelId as string,
lastMessageId as string
);
});
}
}
================================================
FILE: backend/src/controllers/setting.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { SettingService } from "../services/SettingService";
import { BaseController } from "./BaseController";
@injectable()
export class SettingController extends BaseController {
constructor(@inject(TYPES.SettingService) private settingService: SettingService) {
super();
}
async get(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const userId = req.user?.userId;
const role = Number(req.user?.role);
return await this.settingService.getSettings(userId, role);
});
}
async save(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const userId = req.user?.userId;
const role = Number(req.user?.role);
return await this.settingService.saveSettings(userId, role, req.body);
});
}
}
================================================
FILE: backend/src/controllers/sponsors.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { SponsorsService } from "../services/SponsorsService";
import { BaseController } from "./BaseController";
@injectable()
export class SponsorsController extends BaseController {
constructor(@inject(TYPES.SponsorsService) private sponsorsService: SponsorsService) {
super();
}
async get(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
return await this.sponsorsService.getSponsors();
});
}
}
================================================
FILE: backend/src/controllers/teleImages.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { ImageService } from "../services/ImageService";
import { BaseController } from "./BaseController";
@injectable()
export class ImageController extends BaseController {
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {
super();
}
async getImages(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const url = decodeURIComponent((req.query.url as string) || "");
const response = await this.imageService.getImages(url);
// 设置正确的响应头
res.setHeader("Content-Type", response.headers["content-type"]);
res.setHeader("Cache-Control", "no-cache");
// 确保清除任何可能导致304响应的头信息
res.removeHeader("etag");
res.removeHeader("last-modified");
// 直接传输图片数据
response.data.pipe(res);
});
}
}
================================================
FILE: backend/src/controllers/user.ts
================================================
import { Request, Response } from "express";
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import { UserService } from "../services/UserService";
import { BaseController } from "./BaseController";
@injectable()
export class UserController extends BaseController {
constructor(@inject(TYPES.UserService) private userService: UserService) {
super();
}
async register(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { username, password, registerCode } = req.body;
return await this.userService.register(username, password, registerCode);
});
}
async login(req: Request, res: Response): Promise {
await this.handleRequest(req, res, async () => {
const { username, password } = req.body;
return await this.userService.login(username, password);
});
}
}
================================================
FILE: backend/src/core/ApiResponse.ts
================================================
export class ApiResponse {
success: boolean;
data?: T;
message?: string;
code: number;
private constructor(success: boolean, code: number, data?: T, message?: string) {
this.success = success;
this.code = code;
this.data = data;
this.message = message;
}
static success(data?: T, message = "操作成功"): ApiResponse {
return new ApiResponse(true, 0, data, message);
}
static error(message: string, code = 10000): ApiResponse {
return new ApiResponse(false, code, null, message);
}
}
================================================
FILE: backend/src/core/types.ts
================================================
export const TYPES = {
DatabaseService: Symbol.for("DatabaseService"),
Cloud115Service: Symbol.for("Cloud115Service"),
QuarkService: Symbol.for("QuarkService"),
Searcher: Symbol.for("Searcher"),
DoubanService: Symbol.for("DoubanService"),
ImageService: Symbol.for("ImageService"),
SettingService: Symbol.for("SettingService"),
UserService: Symbol.for("UserService"),
SponsorsService: Symbol.for("SponsorsService"),
Cloud115Controller: Symbol.for("Cloud115Controller"),
QuarkController: Symbol.for("QuarkController"),
ResourceController: Symbol.for("ResourceController"),
DoubanController: Symbol.for("DoubanController"),
ImageController: Symbol.for("ImageController"),
SettingController: Symbol.for("SettingController"),
UserController: Symbol.for("UserController"),
SponsorsController: Symbol.for("SponsorsController"),
};
================================================
FILE: backend/src/inversify.config.ts
================================================
import { Container } from "inversify";
import { TYPES } from "./core/types";
// Services
import { DatabaseService } from "./services/DatabaseService";
import { Cloud115Service } from "./services/Cloud115Service";
import { QuarkService } from "./services/QuarkService";
import { Searcher } from "./services/Searcher";
import { DoubanService } from "./services/DoubanService";
import { UserService } from "./services/UserService";
import { ImageService } from "./services/ImageService";
import { SettingService } from "./services/SettingService";
import { SponsorsService } from "./services/SponsorsService";
// Controllers
import { Cloud115Controller } from "./controllers/cloud115";
import { QuarkController } from "./controllers/quark";
import { ResourceController } from "./controllers/resource";
import { DoubanController } from "./controllers/douban";
import { ImageController } from "./controllers/teleImages";
import { SettingController } from "./controllers/setting";
import { UserController } from "./controllers/user";
import { SponsorsController } from "./controllers/sponsors";
const container = new Container();
// Services
container.bind(TYPES.DatabaseService).to(DatabaseService).inSingletonScope();
container.bind(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope();
container.bind(TYPES.QuarkService).to(QuarkService).inSingletonScope();
container.bind(TYPES.Searcher).to(Searcher).inSingletonScope();
container.bind(TYPES.ImageService).to(ImageService).inSingletonScope();
container.bind(TYPES.SettingService).to(SettingService).inSingletonScope();
container.bind(TYPES.DoubanService).to(DoubanService).inSingletonScope();
container.bind(TYPES.UserService).to(UserService).inSingletonScope();
container.bind(TYPES.SponsorsService).to(SponsorsService).inSingletonScope();
// Controllers
container.bind(TYPES.Cloud115Controller).to(Cloud115Controller);
container.bind(TYPES.QuarkController).to(QuarkController);
container.bind(TYPES.ResourceController).to(ResourceController);
container.bind(TYPES.DoubanController).to(DoubanController);
container.bind(TYPES.ImageController).to(ImageController);
container.bind(TYPES.SettingController).to(SettingController);
container.bind(TYPES.UserController).to(UserController);
container.bind(TYPES.SponsorsController).to(SponsorsController);
export { container };
================================================
FILE: backend/src/middleware/auth.ts
================================================
// 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
): Promise => {
if (req.path === "/user/login" || req.path === "/user/register" || req.path === "/tele-images/") {
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" });
}
};
================================================
FILE: backend/src/middleware/cors.ts
================================================
import { Request, Response, NextFunction } from "express";
export const cors = () => {
return (req: Request, res: Response, next: NextFunction) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie");
res.header("Access-Control-Allow-Credentials", "true");
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
next();
};
};
================================================
FILE: backend/src/middleware/errorHandler.ts
================================================
import { Request, Response } from "express";
interface CustomError extends Error {
status?: number;
}
export const errorHandler = (err: CustomError, req: Request, res: Response): void => {
res.status(err.status || 500).json({
success: false,
error: err.message || "服务器内部错误",
});
};
================================================
FILE: backend/src/middleware/index.ts
================================================
import { Application } from "express";
import express from "express";
import { authMiddleware } from "./auth";
import { requestLogger } from "./requestLogger";
import { rateLimiter } from "./rateLimiter";
import { cors } from "./cors";
export const setupMiddlewares = (app: Application) => {
app.use(express.json());
app.use(cors());
app.use(requestLogger());
app.use(rateLimiter());
app.use(authMiddleware);
};
================================================
FILE: backend/src/middleware/rateLimiter.ts
================================================
import { Request, Response, NextFunction } from "express";
const requestCounts = new Map();
const WINDOW_MS = 60 * 1000; // 1分钟窗口
const MAX_REQUESTS = 600; // 每个IP每分钟最多60个请求
export const rateLimiter = () => {
return (req: Request, res: Response, next: NextFunction) => {
const ip = req.ip || req.socket.remoteAddress || "unknown";
const now = Date.now();
const record = requestCounts.get(ip) || { count: 0, timestamp: now };
if (now - record.timestamp > WINDOW_MS) {
record.count = 0;
record.timestamp = now;
}
record.count++;
requestCounts.set(ip, record);
if (record.count > MAX_REQUESTS) {
return res.status(429).json({ message: "请求过于频繁,请稍后再试" });
}
next();
};
};
================================================
FILE: backend/src/middleware/requestLogger.ts
================================================
import { Request, Response, NextFunction } from "express";
import { logger } from "../utils/logger";
const excludePaths = ["/tele-images/"];
export const requestLogger = () => {
return (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
if (excludePaths.includes(req.path)) {
return;
}
const duration = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
};
};
================================================
FILE: backend/src/middleware/validateRequest.ts
================================================
import { Request, Response, NextFunction } from "express";
export const validateRequest = (
requiredParams: string[]
): ((req: Request, res: Response, next: NextFunction) => Response | void) => {
return (req: Request, res: Response, next: NextFunction) => {
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
if (missingParams.length > 0) {
return res.status(400).json({
success: false,
error: `缺少必要的参数: ${missingParams.join(", ")}`,
});
}
next();
};
};
================================================
FILE: backend/src/models/GlobalSetting.ts
================================================
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 {}
class GlobalSetting
extends Model
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;
================================================
FILE: backend/src/models/User.ts
================================================
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 {}
class User extends Model 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;
================================================
FILE: backend/src/models/UserSetting.ts
================================================
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 {}
class UserSetting
extends Model
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;
================================================
FILE: backend/src/routes/api.ts
================================================
import { Router } from "express";
import { container } from "../inversify.config";
import { TYPES } from "../core/types";
import { Cloud115Controller } from "../controllers/cloud115";
import { QuarkController } from "../controllers/quark";
import { ResourceController } from "../controllers/resource";
import { DoubanController } from "../controllers/douban";
import { ImageController } from "../controllers/teleImages";
import { SettingController } from "../controllers/setting";
import { UserController } from "../controllers/user";
import { SponsorsController } from "../controllers/sponsors";
const router = Router();
// 获取控制器实例
const cloud115Controller = container.get(TYPES.Cloud115Controller);
const quarkController = container.get(TYPES.QuarkController);
const resourceController = container.get(TYPES.ResourceController);
const doubanController = container.get(TYPES.DoubanController);
const imageController = container.get(TYPES.ImageController);
const settingController = container.get(TYPES.SettingController);
const userController = container.get(TYPES.UserController);
const sponsorsController = container.get(TYPES.SponsorsController);
// 用户相关路由
router.post("/user/login", (req, res) => userController.login(req, res));
router.post("/user/register", (req, res) => userController.register(req, res));
// 图片相关路由
router.get("/tele-images", (req, res) => imageController.getImages(req, res));
// 设置相关路由
router.get("/setting/get", (req, res) => settingController.get(req, res));
router.post("/setting/save", (req, res) => settingController.save(req, res));
// 资源搜索
router.get("/search", (req, res) => resourceController.search(req, res));
// 获取赞助者列表
router.get("/sponsors", (req, res) => sponsorsController.get(req, res));
// 115网盘相关
router.get("/cloud115/share-info", (req, res) => cloud115Controller.getShareInfo(req, res));
router.get("/cloud115/folders", (req, res) => cloud115Controller.getFolderList(req, res));
router.post("/cloud115/save", (req, res) => cloud115Controller.saveFile(req, res));
// 夸克网盘相关
router.get("/quark/share-info", (req, res) => quarkController.getShareInfo(req, res));
router.get("/quark/folders", (req, res) => quarkController.getFolderList(req, res));
router.post("/quark/save", (req, res) => quarkController.saveFile(req, res));
// 获取豆瓣热门列表
router.get("/douban/hot", (req, res) => doubanController.getDoubanHotList(req, res));
export default router;
================================================
FILE: backend/src/services/Cloud115Service.ts
================================================
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
import { createAxiosInstance } from "../utils/axiosInstance";
import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "../types/cloud";
import { injectable } from "inversify";
import { Request } from "express";
import UserSetting from "../models/UserSetting";
import { ICloudStorageService } from "@/types/services";
import { logger } from "../utils/logger";
interface Cloud115ListItem {
cid: string;
n: string;
s: number;
}
interface Cloud115FolderItem {
cid: string;
n: string;
ns: number;
}
@injectable()
export class Cloud115Service implements ICloudStorageService {
private api: AxiosInstance;
private cookie: string = "";
constructor() {
this.api = createAxiosInstance(
"https://webapi.115.com",
AxiosHeaders.from({
Host: "webapi.115.com",
Connection: "keep-alive",
xweb_xhr: "1",
Origin: "",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/6.8.0(0x16080000) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.9(0x13080910) XWEB/1227",
Accept: "*/*",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
})
);
this.api.interceptors.request.use((config) => {
config.headers.cookie = this.cookie;
return config;
});
}
async setCookie(req: Request): Promise {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.cloud115Cookie) {
this.cookie = userSetting.dataValues.cloud115Cookie;
} else {
throw new Error("请先设置115网盘cookie");
}
}
async getShareInfo(shareCode: string, receiveCode = ""): Promise {
const response = await this.api.get("/share/snap", {
params: {
share_code: shareCode,
receive_code: receiveCode,
offset: 0,
limit: 20,
cid: "",
},
});
if (response.data?.state && response.data.data?.list?.length > 0) {
return {
data: {
list: response.data.data.list.map((item: Cloud115ListItem) => ({
fileId: item.cid,
fileName: item.n,
fileSize: item.s,
})),
},
};
} else {
logger.error("未找到文件信息:", response.data);
throw new Error("未找到文件信息");
}
}
async getFolderList(parentCid = "0"): Promise {
const response = await this.api.get("/files", {
params: {
aid: 1,
cid: parentCid,
o: "user_ptime",
asc: 1,
offset: 0,
show_dir: 1,
limit: 50,
type: 0,
format: "json",
star: 0,
suffix: "",
natsort: 0,
snap: 0,
record_open_time: 1,
fc_mix: 0,
},
});
if (response.data?.state) {
return {
data: response.data.data
.filter((item: Cloud115FolderItem) => item.cid && !!item.ns)
.map((folder: Cloud115FolderItem) => ({
cid: folder.cid,
name: folder.n,
path: response.data.path,
})),
};
} else {
logger.error("获取目录列表失败:", response.data.error);
throw new Error("获取115pan目录列表失败:" + response.data.error);
}
}
async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {
const param = new URLSearchParams({
cid: params.folderId || "",
share_code: params.shareCode || "",
receive_code: params.receiveCode || "",
file_id: params.fids?.[0] || "",
});
const response = await this.api.post("/share/receive", param.toString());
logger.info("保存文件:", response.data);
if (response.data.state) {
return {
message: response.data.error,
data: response.data.data,
};
} else {
logger.error("保存文件失败:", response.data.error);
throw new Error("保存115pan文件失败:" + response.data.error);
}
}
}
================================================
FILE: backend/src/services/DatabaseService.ts
================================================
import { Sequelize, QueryTypes } from "sequelize";
import GlobalSetting from "../models/GlobalSetting";
import { Searcher } from "./Searcher";
import sequelize from "../config/database";
// 全局设置默认值
const DEFAULT_GLOBAL_SETTINGS = {
httpProxyHost: "127.0.0.1",
httpProxyPort: 7890,
isProxyEnabled: false,
CommonUserCode: 9527,
AdminUserCode: 230713,
};
export class DatabaseService {
private sequelize: Sequelize;
constructor() {
this.sequelize = sequelize;
}
async initialize(): Promise {
try {
await this.sequelize.query("PRAGMA foreign_keys = OFF");
await this.cleanupBackupTables();
await this.sequelize.sync({ alter: true });
await this.sequelize.query("PRAGMA foreign_keys = ON");
await this.initializeGlobalSettings();
} catch (error) {
throw new Error(`数据库初始化失败: ${(error as Error).message}`);
}
}
private async initializeGlobalSettings(): Promise {
try {
const settings = await GlobalSetting.findOne();
if (!settings) {
await GlobalSetting.create(DEFAULT_GLOBAL_SETTINGS);
console.log("✅ Global settings initialized with default values.");
}
await Searcher.updateAxiosInstance();
} catch (error) {
console.error("❌ Failed to initialize global settings:", error);
throw error;
}
}
private async cleanupBackupTables(): Promise {
const backupTables = await this.sequelize.query<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\_backup%' ESCAPE '\\'",
{ type: QueryTypes.SELECT }
);
for (const table of backupTables) {
if (table?.name) {
await this.sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);
}
}
}
// ... 其他数据库相关方法
}
================================================
FILE: backend/src/services/DoubanService.ts
================================================
import { AxiosHeaders, AxiosInstance } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance";
interface DoubanSubject {
id: string;
title: string;
rate: string;
cover: string;
url: string;
is_new: boolean;
}
export 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;
}): Promise<{ data: DoubanSubject[] }> {
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;
}
}
}
================================================
FILE: backend/src/services/ImageService.ts
================================================
import { injectable } from "inversify";
import axios, { AxiosInstance } from "axios";
import tunnel from "tunnel";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
@injectable()
export class ImageService {
private axiosInstance: AxiosInstance | null = null;
constructor() {
// 移除构造函数中的初始化,改为懒加载
}
private async ensureAxiosInstance(): Promise {
if (!this.axiosInstance) {
const settings = await GlobalSetting.findOne();
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = axios.create({
timeout: 30000,
headers: {
Accept: "image/*, */*",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
},
withCredentials: false,
maxRedirects: 5,
httpsAgent: globalSetting.isProxyEnabled
? tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
})
: undefined,
});
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
throw error;
}
);
}
return this.axiosInstance;
}
async updateAxiosInstance(): Promise {
this.axiosInstance = null;
await this.ensureAxiosInstance();
}
async getImages(url: string): Promise {
const axiosInstance = await this.ensureAxiosInstance();
return await axiosInstance.get(url, {
responseType: "stream",
validateStatus: (status) => status >= 200 && status < 300,
headers: {
Referer: new URL(url).origin,
},
});
}
}
================================================
FILE: backend/src/services/QuarkService.ts
================================================
import { AxiosInstance, AxiosHeaders } from "axios";
import { logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
import { injectable } from "inversify";
import { Request } from "express";
import UserSetting from "../models/UserSetting";
import {
ShareInfoResponse,
FolderListResponse,
QuarkFolderItem,
SaveFileParams,
} from "../types/cloud";
import { ICloudStorageService } from "@/types/services";
interface QuarkShareInfo {
stoken?: string;
pwdId?: string;
fileSize?: number;
list: {
fid: string;
file_name: string;
file_type: number;
share_fid_token: string;
}[];
}
@injectable()
export class QuarkService implements ICloudStorageService {
private api: AxiosInstance;
private cookie: string = "";
constructor() {
this.api = createAxiosInstance(
"https://drive-h.quark.cn",
AxiosHeaders.from({
cookie: this.cookie,
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",
"content-type": "application/json",
priority: "u=1, i",
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
})
);
this.api.interceptors.request.use((config) => {
config.headers.cookie = this.cookie;
return config;
});
}
async setCookie(req: Request): Promise {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.quarkCookie) {
this.cookie = userSetting.dataValues.quarkCookie;
} else {
throw new Error("请先设置夸克网盘cookie");
}
}
async getShareInfo(pwdId: string, passcode = ""): Promise {
const response = await this.api.post(
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
{
pwd_id: pwdId,
passcode,
}
);
if (response.data?.status === 200 && response.data.data) {
const fileInfo = response.data.data;
if (fileInfo.stoken) {
const res = await this.getShareList(pwdId, fileInfo.stoken);
return {
data: res,
};
}
}
throw new Error("获取夸克分享信息失败");
}
async getShareList(pwdId: string, stoken: string): Promise {
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
params: {
pr: "ucpro",
fr: "pc",
uc_param_str: "",
pwd_id: pwdId,
stoken: stoken,
pdir_fid: "0",
force: "0",
_page: "1",
_size: "50",
_fetch_banner: "1",
_fetch_share: "1",
_fetch_total: "1",
_sort: "file_type:asc,updated_at:desc",
__dt: "1589",
__t: Date.now(),
},
});
if (response.data?.data) {
const list = response.data.data.list
.filter((item: QuarkShareInfo["list"][0]) => item.fid)
.map((folder: QuarkShareInfo["list"][0]) => ({
fileId: folder.fid,
fileName: folder.file_name,
fileIdToken: folder.share_fid_token,
}));
return {
list,
pwdId,
stoken,
fileSize: response.data.data.share?.size || 0,
};
} else {
return {
list: [],
};
}
}
async getFolderList(parentCid = "0"): Promise {
const response = await this.api.get("/1/clouddrive/file/sort", {
params: {
pr: "ucpro",
fr: "pc",
uc_param_str: "",
pdir_fid: parentCid,
_page: "1",
_size: "100",
_fetch_total: "false",
_fetch_sub_dirs: "1",
_sort: "",
__dt: "2093126",
__t: Date.now(),
},
});
if (response.data?.data && response.data.data.list) {
const data = response.data.data.list
.filter((item: QuarkFolderItem) => item.fid && item.file_type === 0)
.map((folder: QuarkFolderItem) => ({
cid: folder.fid,
name: folder.file_name,
path: [],
}));
return {
data,
};
} else {
const message = "获取夸克目录列表失败:" + response.data.error;
logger.error(message);
throw new Error(message);
}
}
async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {
const quarkParams = {
fid_list: params.fids,
fid_token_list: params.fidTokens,
to_pdir_fid: params.folderId,
pwd_id: params.shareCode,
stoken: params.receiveCode,
pdir_fid: "0",
scene: "link",
};
try {
const response = await this.api.post(
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,
quarkParams
);
return {
message: response.data.message,
data: response.data.data,
};
} catch (error) {
throw new Error(error instanceof Error ? error.message : "未知错误");
}
}
}
================================================
FILE: backend/src/services/Searcher.ts
================================================
import { AxiosInstance, AxiosHeaders } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
import * as cheerio from "cheerio";
import { config } from "../config";
import { logger } from "../utils/logger";
import { injectable } from "inversify";
interface sourceItem {
messageId?: string;
title?: string;
completeTitle?: string;
link?: string;
pubDate?: string;
content?: string;
description?: string;
image?: string;
cloudLinks?: string[];
tags?: string[];
cloudType?: string;
}
@injectable()
export class Searcher {
private static instance: Searcher;
private api: AxiosInstance | null = null;
constructor() {
this.initAxiosInstance();
Searcher.instance = this;
}
private async initAxiosInstance(isUpdate: boolean = false) {
let globalSetting = {} as GlobalSettingAttributes;
if (isUpdate) {
const settings = await GlobalSetting.findOne();
globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
}
this.api = createAxiosInstance(
config.telegram.baseUrl,
AxiosHeaders.from({
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "max-age=0",
priority: "u=0, i",
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
}),
globalSetting?.isProxyEnabled,
globalSetting?.isProxyEnabled
? { host: globalSetting?.httpProxyHost, port: globalSetting?.httpProxyPort }
: undefined
);
}
public static async updateAxiosInstance(): Promise {
await Searcher.instance.initAxiosInstance(true);
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
const links: string[] = [];
let cloudType = "";
Object.values(config.cloudPatterns).forEach((pattern, index) => {
const matches = text.match(pattern);
if (matches) {
links.push(...matches);
if (!cloudType) cloudType = Object.keys(config.cloudPatterns)[index];
}
});
return {
links: [...new Set(links)],
cloudType,
};
}
async searchAll(keyword: string, channelId?: string, messageId?: string) {
const allResults: any[] = [];
const channelList: any[] = channelId
? config.telegram.channels.filter((channel: any) => channel.id === channelId)
: config.telegram.channels;
// 使用Promise.all进行并行请求
const searchPromises = channelList.map(async (channel) => {
try {
const messageIdparams = messageId ? `before=${messageId}` : "";
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
console.log(`Searching in channel ${channel.name} with URL: ${url}`);
return this.searchInWeb(url).then((results) => {
console.log(`Found ${results.items.length} items in channel ${channel.name}`);
if (results.items.length > 0) {
const channelResults = results.items
.filter((item: sourceItem) => item.cloudLinks && item.cloudLinks.length > 0)
.map((item: sourceItem) => ({
...item,
channel: channel.name,
channelId: channel.id,
}));
allResults.push({
list: channelResults,
channelInfo: {
...channel,
channelLogo: results.channelLogo,
},
id: channel.id,
});
}
});
} catch (error) {
logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
});
// 等待所有请求完成
await Promise.all(searchPromises);
return {
data: allResults,
};
}
async searchInWeb(url: string) {
try {
if (!this.api) {
throw new Error("Axios instance is not initialized");
}
const response = await this.api.get(url);
const html = response.data;
const $ = cheerio.load(html);
const items: sourceItem[] = [];
let channelLogo = "";
$(".tgme_header_link").each((_, element) => {
channelLogo = $(element).find("img").attr("src") || "";
});
// 遍历每个消息容器
$(".tgme_widget_message_wrap").each((_, element) => {
const messageEl = $(element);
// 通过 data-post 属性来获取消息的链接 去除channelId 获得消息id
const messageId = messageEl
.find(".tgme_widget_message")
.data("post")
?.toString()
.split("/")[1];
// 提取标题 (第一个
标签前的内容)
const title =
messageEl
.find(".js-message_text")
.html()
?.split("
")[0]
.replace(/<[^>]+>/g, "")
.replace(/\n/g, "") || "";
// 提取描述 (第一个标签前面的内容,不包含标题)
const content =
messageEl
.find(".js-message_text")
.html()
?.replace(title, "")
.split("/g, "")
.trim() || "";
// 提取链接 (消息中的链接)
// const link = messageEl.find('.tgme_widget_message').data('post');
// 提取发布时间
const pubDate = messageEl.find("time").attr("datetime");
// 提取图片
const image = messageEl
.find(".tgme_widget_message_photo_wrap")
.attr("style")
?.match(/url\('(.+?)'\)/)?.[1];
const tags: string[] = [];
// 提取云盘链接
const links = messageEl
.find(".tgme_widget_message_text a")
.map((_, el) => $(el).attr("href"))
.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(" "));
// 添加到数组第一位
items.unshift({
messageId,
title,
pubDate,
content,
image,
cloudLinks: cloudInfo.links,
cloudType: cloudInfo.cloudType,
tags,
});
});
return { items: items, channelLogo };
} catch (error) {
logger.error(`搜索错误: ${url}`, error);
return {
items: [],
channelLogo: "",
};
}
}
}
export default new Searcher();
================================================
FILE: backend/src/services/SettingService.ts
================================================
import { injectable, inject } from "inversify";
import { TYPES } from "../core/types";
import UserSetting from "../models/UserSetting";
import GlobalSetting from "../models/GlobalSetting";
import { Searcher } from "./Searcher";
import { ImageService } from "./ImageService";
@injectable()
export class SettingService {
constructor(@inject(TYPES.ImageService) private imageService: ImageService) {}
async getSettings(userId: string | undefined, role: number | undefined) {
if (!userId) {
throw new Error("用户ID无效");
}
let userSettings = await UserSetting.findOne({ where: { userId: userId.toString() } });
if (!userSettings) {
userSettings = await UserSetting.create({
userId: userId.toString(),
cloud115Cookie: "",
quarkCookie: "",
});
}
const globalSetting = await GlobalSetting.findOne();
return {
data: {
userSettings,
globalSetting: role === 1 ? globalSetting : null,
},
};
}
async saveSettings(userId: string | undefined, role: number | undefined, settings: any) {
if (!userId) {
throw new Error("用户ID无效");
}
const { userSettings, globalSetting } = settings;
await UserSetting.update(userSettings, { where: { userId: userId.toString() } });
if (role === 1 && globalSetting) {
await GlobalSetting.update(globalSetting, { where: {} });
}
await this.updateSettings();
return { message: "保存成功" };
}
async updateSettings(/* 参数 */): Promise {
// ... 其他代码 ...
// 修改这一行,使用注入的实例方法而不是静态方法
await this.imageService.updateAxiosInstance();
await Searcher.updateAxiosInstance();
// ... 其他代码 ...
}
}
================================================
FILE: backend/src/services/SponsorsService.ts
================================================
import { injectable } from "inversify";
import { createAxiosInstance } from "../utils/axiosInstance";
import { AxiosInstance } from "axios";
import sponsors from "../sponsors/sponsors.json";
@injectable()
export class SponsorsService {
private axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = createAxiosInstance("http://oss.jiangmuxin.cn/cloudsaver/");
}
async getSponsors() {
try {
const response = await this.axiosInstance.get("sponsors.json");
return {
data: response.data.sponsors,
};
} catch (error) {
return {
data: sponsors.sponsors,
};
}
}
}
================================================
FILE: backend/src/services/UserService.ts
================================================
import { injectable } from "inversify";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { config } from "../config";
import User from "../models/User";
import GlobalSetting from "../models/GlobalSetting";
@injectable()
export class UserService {
private isValidInput(input: string): boolean {
// 检查是否包含空格或汉字
const regex = /^[^\s\u4e00-\u9fa5]+$/;
return regex.test(input);
}
async register(username: string, password: string, registerCode: string) {
const globalSetting = await GlobalSetting.findOne();
const registerCodeList = [
globalSetting?.dataValues.CommonUserCode,
globalSetting?.dataValues.AdminUserCode,
];
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
throw new Error("注册码错误");
}
// 验证输入
if (!this.isValidInput(username) || !this.isValidInput(password)) {
throw new Error("用户名、密码或注册码不能包含空格或汉字");
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
throw new Error("用户名已存在");
}
const hashedPassword = await bcrypt.hash(password, 10);
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
const user = await User.create({ username, password: hashedPassword, role });
return {
data: user,
message: "用户注册成功",
};
}
async login(username: string, password: string) {
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error("用户名或密码错误");
}
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
expiresIn: "6h",
});
return {
data: {
token,
},
};
}
}
================================================
FILE: backend/src/sponsors/sponsors.json
================================================
{
"sponsors": [
{
"name": "立本狗头",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks1.jpg",
"message": "怒搓楼上狗头! "
},
{
"name": "帝国鼻屎",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks2.jpg",
"message": "芜湖起飞! "
},
{
"name": "雷霆222",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks3.jpg",
"message": "把我弄帅点 "
},
{
"name": "黑田奈奈子",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks4.jpg",
"message": "流年笑掷 未来可期 ",
"link": "https://github.com/htnanako"
},
{
"name": "原野🐇",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks5.jpg"
},
{
"name": "我摆烂!",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks6.jpg",
"message": "人生苦短,及时行乐,卷什么卷,随缘摆烂 "
},
{
"name": "田培",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks7.jpg"
},
{
"name": "River",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks8.jpg"
},
{
"name": "午夜学徒",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks9.jpg"
},
{
"name": "阿潘",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks10.jpg"
},
{
"name": "闹闹黑",
"avatar": "http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks11.jpg"
}
]
}
================================================
FILE: backend/src/types/cloud.ts
================================================
export interface ShareInfoResponse {
data: {
list: ShareInfoItem[];
fileSize?: number;
pwdId?: string;
stoken?: string;
};
}
export interface GetShareInfoParams {
shareCode: string;
receiveCode?: string;
}
export interface ShareInfoItem {
fileId: string;
fileName: string;
fileSize?: number;
fileIdToken?: string;
}
export interface FolderListResponse {
data: {
cid: string;
name: string;
path: { cid: string; name: string }[];
}[];
}
export interface SaveFileParams {
shareCode: string; // 分享code
receiveCode?: string; // 分享文件的密码
folderId?: string; // 文件夹id
fids?: string[]; // 存储文件id
fidTokens?: string[]; // 存储文件token
}
export interface SaveFileResponse {
message: string;
data: unknown;
}
export interface ShareFileInfo {
shareCode: string;
receiveCode?: string;
fileId: string;
cid?: string;
fid_list?: string[];
fid_token_list?: string[];
to_pdir_fid?: string;
pwd_id?: string;
stoken?: string;
pdir_fid?: string;
scene?: string;
[key: string]: any;
}
export interface QuarkShareFileInfo {
fid_list: string[];
fid_token_list: string[];
to_pdir_fid: string;
pwd_id: string;
stoken: string;
pdir_fid: string;
scene: string;
}
export interface QuarkShareInfo {
stoken?: string;
pwdId?: string;
fileSize?: number;
list: {
fid: string;
file_name: string;
file_type: number;
share_fid_token: string;
}[];
}
export interface QuarkFolderItem {
fid: string;
file_name: string;
file_type: number;
}
================================================
FILE: backend/src/types/cloud115.ts
================================================
export interface ShareInfo {
fileId: string;
fileName: string;
fileSize: number;
}
export interface ShareInfoResponse {
data?: ShareInfo[];
message?: string;
}
================================================
FILE: backend/src/types/express.ts
================================================
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Request } from "express";
declare module "express" {
interface Request {
user?: {
userId: string;
role: number;
};
}
}
================================================
FILE: backend/src/types/index.ts
================================================
export interface Config {
app: {
port: number;
env: string;
};
database: {
type: string;
path: string;
};
jwt: {
secret: string;
expiresIn: string;
};
// ... 其他配置类型
}
================================================
FILE: backend/src/types/services.ts
================================================
import { Request } from "express";
import { ShareInfoResponse, FolderListResponse, SaveFileParams } from "./cloud";
export interface ICloudStorageService {
setCookie(req: Request): Promise;
getShareInfo(shareCode: string, receiveCode?: string): Promise;
getFolderList(parentCid?: string): Promise;
saveSharedFile(params: SaveFileParams): Promise;
}
================================================
FILE: backend/src/utils/axiosInstance.ts
================================================
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
import tunnel from "tunnel";
interface ProxyConfig {
host: string;
port: number;
}
export function createAxiosInstance(
baseURL: string,
headers?: AxiosRequestHeaders,
useProxy: boolean = false,
proxyConfig?: ProxyConfig
): AxiosInstance {
let agent;
if (useProxy && proxyConfig) {
agent = tunnel.httpsOverHttp({
proxy: proxyConfig,
});
}
return axios.create({
baseURL,
timeout: 30000,
headers,
httpsAgent: useProxy ? agent : undefined,
withCredentials: true,
});
}
================================================
FILE: backend/src/utils/handleError.ts
================================================
import { Response, NextFunction } from "express";
import { logger } from "../utils/logger";
interface CustomError {
name?: string;
message: string;
success?: boolean;
}
export default function handleError(
res: Response,
error: CustomError | unknown,
message: string,
next: NextFunction
) {
logger.error(message, error);
next(error || { success: false, message });
}
================================================
FILE: backend/src/utils/index.ts
================================================
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;
}
}
================================================
FILE: backend/src/utils/logger.ts
================================================
import winston from "winston";
import { config } from "../config";
const logger = winston.createLogger({
level: config.app.env === "development" ? "debug" : "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" }),
],
});
if (config.app.env !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}
export { logger };
================================================
FILE: backend/src/utils/response.ts
================================================
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);
};
================================================
FILE: backend/src/utils/responseHandler.ts
================================================
import { Response } from "express";
export const handleResponse = (res: Response, data: any, success: boolean) => {
res.json({ success, data });
};
================================================
FILE: backend/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/@types", "./src/types"],
"paths": {
"@/*": ["src/*"]
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
================================================
FILE: docker-entrypoint.sh
================================================
#!/bin/sh
# 如果配置目录下没有 env 文件,则复制示例文件
if [ ! -f /app/config/env ]; then
cp /app/.env.example /app/config/env
echo "已创建默认配置文件 /app/config/env,请根据需要修改配置"
fi
# 创建配置文件软链接
ln -sf /app/config/env /app/.env
# 启动 Nginx 和后端服务
nginx -g 'daemon off;' & npm run start
================================================
FILE: frontend/.env
================================================
VITE_API_BASE_URL=""
VITE_API_BASE_URL_PROXY="http://127.0.0.1:8009"
================================================
FILE: frontend/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
}
================================================
FILE: frontend/components.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
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']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
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']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
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']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']
ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']
ResourceSelect: typeof import('./src/components/Home/ResourceSelect.vue')['default']
ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
VanBackTop: typeof import('vant/es')['BackTop']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanLoading: typeof import('vant/es')['Loading']
VanOverlay: typeof import('vant/es')['Overlay']
VanPopup: typeof import('vant/es')['Popup']
VanSearch: typeof import('vant/es')['Search']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
================================================
FILE: frontend/index.html
================================================
CloudSaver
================================================
FILE: frontend/package.json
================================================
{
"name": "cloud-saver-web",
"private": true,
"version": "0.2.5",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.7",
"element-plus": "^2.6.1",
"gsap": "^3.12.7",
"pinia": "^2.1.7",
"socket.io-client": "^4.8.1",
"typeit": "^8.8.7",
"vant": "^4.9.17",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/node": "^20.11.25",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^5.0.4",
"postcss-pxtorem": "^6.1.0",
"sass": "^1.83.4",
"typescript": "^5.4.2",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.5",
"vite-plugin-pwa": "^0.21.1",
"vue-tsc": "^2.0.6"
}
}
================================================
FILE: frontend/postcss.config.cjs
================================================
module.exports = {
plugins: {
"postcss-pxtorem": {
rootValue({ file }) {
return file.indexOf("vant") !== -1 || file.indexOf("mobile") !== -1 ? 50 : 75;
},
propList: ["*"],
exclude: (file) => {
return !file.includes("mobile") && !file.includes("vant");
},
minPixelValue: 2,
mediaQuery: false,
},
},
};
================================================
FILE: frontend/src/App.vue
================================================
================================================
FILE: frontend/src/api/cloud115.ts
================================================
import request from "@/utils/request";
import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types";
export const cloud115Api = {
async getShareInfo(params: GetShareInfoParams) {
const { data } = await request.get("/api/cloud115/share-info", {
params,
});
return data as ShareInfoResponse;
},
async getFolderList(parentCid = "0") {
const res = await request.get("/api/cloud115/folders", {
params: { parentCid },
});
return res;
},
async saveFile(params: SaveFileParams) {
const res = await request.post("/api/cloud115/save", params);
return res;
},
};
================================================
FILE: frontend/src/api/douban.ts
================================================
import request from "@/utils/request";
import { HotListItem, HotListParams } from "@/types/douban";
export const doubanApi = {
async getHotList(params: HotListParams) {
const { data } = await request.get("/api/douban/hot", {
params,
});
return data;
},
};
================================================
FILE: frontend/src/api/quark.ts
================================================
import request from "@/utils/request";
import type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from "@/types";
export const quarkApi = {
async getShareInfo(params: GetShareInfoParams) {
const { data } = await request.get("/api/quark/share-info", {
params,
});
return data as ShareInfoResponse;
},
async getFolderList(parentCid = "0") {
const data = await request.get("/api/quark/folders", {
params: { parentCid },
});
return data;
},
async saveFile(params: SaveFileParams) {
return await request.post("/api/quark/save", params);
},
};
================================================
FILE: frontend/src/api/resource.ts
================================================
import request from "@/utils/request";
import type { Resource } from "@/types/index";
export const resourceApi = {
search(keyword: string, channelId?: string, lastMessageId?: string) {
return request.get(`/api/search`, {
params: { keyword, channelId, lastMessageId },
});
},
};
================================================
FILE: frontend/src/api/setting.ts
================================================
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);
},
};
================================================
FILE: frontend/src/api/user.ts
================================================
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; registerCode: string }) => {
return request.post<{ token: string }>("/api/user/register", data);
},
getSponsors: () => {
return request.get("/api/sponsors?timestamp=" + Date.now());
},
};
================================================
FILE: frontend/src/components/AsideMenu.vue
================================================
Cloud Saver
================================================
FILE: frontend/src/components/Home/FolderSelect.vue
================================================
================================================
FILE: frontend/src/components/Home/ResourceCard.vue
================================================
{{ currentResource.cloudType }}
{{ currentResource.title }}
{{ group.channelInfo.name }}
({{ group.list.length }})
================================================
FILE: frontend/src/components/Home/ResourceSelect.vue
================================================
{{ file.fileName }}
{{ formattedFileSize(file.fileSize) }}
================================================
FILE: frontend/src/components/Home/ResourceTable.vue
================================================
{{ row.title }}
标签:
{{ item }}
无
{{ row.cloudType }}
跳转
转存
加载更多
================================================
FILE: frontend/src/components/SearchBar.vue
================================================
================================================
FILE: frontend/src/components/mobile/FolderSelect.vue
================================================
================================================
FILE: frontend/src/components/mobile/ResourceCard.vue
================================================
================================================
FILE: frontend/src/components/mobile/ResourceSelect.vue
================================================
{{ item.fileName }}
{{ formattedFileSize(item.fileSize) }}
================================================
FILE: frontend/src/constants/project.ts
================================================
export const PROJECT_NAME = "Cloudsaver";
export const PROJECT_GITHUB = "https://github.com/jiangrui1994/cloudsaver";
================================================
FILE: frontend/src/constants/storage.ts
================================================
export const STORAGE_KEYS = {
USERNAME: "saved_username",
PASSWORD: "saved_password",
TOKEN: "token",
} as const;
================================================
FILE: frontend/src/env.d.ts
================================================
///
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent, Record, unknown>;
export default component;
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
================================================
FILE: frontend/src/main.ts
================================================
import { createApp } from "vue";
import { createPinia } from "pinia";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import { isMobileDevice } from "@/utils/index";
import App from "./App.vue";
import { Lazyload } from "vant";
import "vant/es/notify/style";
import "vant/es/dialog/style";
import "@/styles/responsive.scss";
import "@/styles/common.scss";
import router from "./router/index";
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(createPinia());
app.use(Lazyload);
app.use(router);
app.use(ElementPlus, {
locale: zhCn,
});
app.mount("#app");
const setRootFontSize = () => {
const isMobile = isMobileDevice();
if (!isMobile) {
return;
} // PC端不干预
const clientWidth = document.documentElement.clientWidth;
const baseSize = clientWidth / 7.5; // 按750px设计稿
document.documentElement.style.fontSize = baseSize + "px";
};
// 初始化执行
setRootFontSize();
// 监听窗口变化
window.addEventListener("resize", setRootFontSize);
================================================
FILE: frontend/src/router/index.ts
================================================
import { createRouter, createWebHistory } from "vue-router";
import mobileRoutes from "./mobile-routes";
import pcRoutes from "./pc-routes";
import { isMobileDevice } from "@/utils/index";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [...(isMobileDevice() ? mobileRoutes : pcRoutes)],
});
export default router;
================================================
FILE: frontend/src/router/mobile-routes.ts
================================================
import type { RouteRecordRaw } from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/",
name: "home",
component: () => import("@/views/mobile/Home.vue"),
redirect: "/resource",
children: [
{
path: "/resource",
name: "resource",
component: () => import("@/views/mobile/ResourceList.vue"),
},
{
path: "/douban",
name: "douban",
component: () => import("@/views/mobile/Douban.vue"),
},
{
path: "/setting",
name: "setting",
component: () => import("@/views/mobile/Setting.vue"),
},
{
path: "/thanks",
name: "thanks",
redirect: "/resource",
},
],
},
{
path: "/login",
name: "login",
component: () => import("@/views/mobile/Login.vue"),
},
];
export default routes;
================================================
FILE: frontend/src/router/pc-routes.ts
================================================
import type { RouteRecordRaw } from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/",
name: "home",
component: () => import("@/views/Home.vue"),
redirect: "/resource",
children: [
{
path: "/resource",
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: "/thanks",
name: "thanks",
component: () => import("@/views/Thanks.vue"),
},
],
},
{
path: "/login",
name: "login",
component: () => import("@/views/pc/Login.vue"),
},
];
export default routes;
================================================
FILE: frontend/src/stores/douban.ts
================================================
import { defineStore } from "pinia";
import { doubanApi } from "@/api/douban";
import { HotListItem } from "@/types/douban";
import { ElMessage } from "element-plus";
interface StoreType {
hotList: HotListItem[];
loading: boolean;
currentParams: CurrentParams;
}
interface CurrentParams {
type: string;
tag?: string;
}
export const useDoubanStore = defineStore("douban", {
state: (): StoreType => ({
hotList: [],
loading: false,
currentParams: {
type: "movie",
tag: "热门",
},
}),
actions: {
async getHotList() {
this.loading = true;
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 || "获取热门列表失败");
} finally {
this.loading = false;
}
},
setCurrentParams(currentParams: CurrentParams) {
this.currentParams = currentParams;
this.getHotList();
},
},
});
================================================
FILE: frontend/src/stores/index.ts
================================================
import { defineStore } from "pinia";
interface StoreType {
scrollTop: boolean;
}
export const useStore = defineStore("global", {
state: (): StoreType => ({
scrollTop: true,
}),
actions: {
setScrollTop(top: boolean) {
this.scrollTop = top;
},
},
});
================================================
FILE: frontend/src/stores/resource.ts
================================================
import { defineStore } from "pinia";
import { cloud115Api } from "@/api/cloud115";
import { resourceApi } from "@/api/resource";
import { quarkApi } from "@/api/quark";
import type {
Resource,
ShareInfoResponse,
ShareInfo,
ResourceItem,
GetShareInfoParams,
SaveFileParams,
ShareFileInfoAndFolder,
} from "@/types";
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 {
name: string;
type: string;
regex: RegExp;
api: {
getShareInfo: (params: GetShareInfoParams) => Promise;
saveFile: (params: SaveFileParams) => Promise<{ code: number; message?: string }>;
};
parseShareCode: (match: RegExpMatchArray) => GetShareInfoParams;
getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => SaveFileParams;
}
// 云盘类型配置
export const CLOUD_DRIVES: CloudDriveConfig[] = [
{
name: "115网盘",
type: "pan115",
regex: /(?:115|anxia|115cdn)\.com\/s\/([^?]+)(?:\?password=([^]+))?/,
api: {
getShareInfo: (params: GetShareInfoParams) => cloud115Api.getShareInfo(params),
saveFile: async (params: SaveFileParams) => {
return await cloud115Api.saveFile(params);
},
},
parseShareCode: (match: RegExpMatchArray) => ({
shareCode: match[1],
receiveCode: match[2] || "",
}),
getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({
shareCode: shareInfoAndFolder.shareCode || "",
receiveCode: shareInfoAndFolder.receiveCode || "",
fileId: shareInfoAndFolder.shareInfo.list[0].fileId,
folderId: shareInfoAndFolder.folderId,
fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""),
}),
},
{
name: "夸克网盘",
type: "quark",
regex: /pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/,
api: {
getShareInfo: (params) => quarkApi.getShareInfo(params),
saveFile: async (params: SaveFileParams) => {
return await quarkApi.saveFile(params);
},
},
parseShareCode: (match: RegExpMatchArray) => ({ shareCode: match[1] }),
getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({
fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || ""),
fidTokens: shareInfoAndFolder.shareInfo.list.map(
(item: { fileIdToken?: string }) => item.fileIdToken || ""
),
folderId: shareInfoAndFolder.folderId,
shareCode: shareInfoAndFolder.shareInfo.pwdId || "",
receiveCode: shareInfoAndFolder.shareInfo.stoken || "",
}),
},
];
export const useResourceStore = defineStore("resource", {
state: () => ({
tagColor: {
baiduPan: "primary",
weiyun: "info",
aliyun: "warning",
pan115: "danger",
quark: "success",
},
keyword: "",
resources: lastResource.list,
lastUpdateTime: lastResource.lastUpdateTime || "",
shareInfo: {} as ShareInfoResponse,
resourceSelect: [] as ShareInfo[],
loading: false,
backupPlan: false,
loadTree: false,
}),
actions: {
setLoadTree(loadTree: boolean) {
this.loadTree = loadTree;
},
// 搜索资源
async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise {
this.loading = true;
if (!isLoadMore) this.resources = [];
try {
let lastMessageId = "";
if (isLoadMore) {
const list = this.resources.find((x) => x.id === channelId)?.list || [];
lastMessageId = list[list.length - 1].messageId || "";
if (list[list.length - 1].isLastMessage) {
ElMessage.warning("没有更多了~");
return;
}
if (!lastMessageId) {
ElMessage.error("当次搜索源不支持加载更多");
return;
}
keyword = this.keyword;
}
let { data = [] } = await resourceApi.search(keyword || "", channelId, lastMessageId);
this.keyword = keyword || "";
data = data
.filter((item) => item.list.length > 0)
.map((x) => ({
...x,
list: x.list.map((item) => ({
...item,
isSupportSave: CLOUD_DRIVES.some((drive) => drive.regex.test(item.cloudLinks[0])),
})),
}));
console.log(data);
if (isLoadMore) {
const findedIndex = this.resources.findIndex((item) => item.id === data[0]?.id);
if (findedIndex !== -1) {
this.resources[findedIndex].list.push(...data[0].list);
}
if (data.length === 0) {
const list = this.resources.find((item) => item.id === channelId)?.list;
list && list[list.length - 1] && (list[list.length - 1]!.isLastMessage = true);
ElMessage.warning("没有更多了~");
}
} else {
this.resources = data.map((item, index) => ({ ...item, displayList: index === 0 }));
if (!keyword) {
// 获取当前时间字符串 用于存储到本地
this.lastUpdateTime = new Date().toLocaleString();
localStorage.setItem(
"last_resource_list",
JSON.stringify({ list: this.resources, lastUpdateTime: this.lastUpdateTime })
);
}
if (this.resources.length === 0) {
ElMessage.warning("未搜索到相关资源");
}
}
} catch (error) {
console.log(error);
this.handleError("搜索失败,请重试", null);
} finally {
this.loading = false;
}
},
// 设置选择资源
async setSelectedResource(resourceSelect: ShareInfo[]) {
this.resourceSelect = resourceSelect;
},
// 转存资源
async saveResource(resource: ResourceItem, folderId: string): Promise {
const savePromises: Promise[] = [];
CLOUD_DRIVES.forEach((drive) => {
if (resource.cloudLinks.some((link) => drive.regex.test(link))) {
savePromises.push(this.saveResourceToDrive(resource, folderId, drive));
}
});
await Promise.all(savePromises);
},
// 保存资源到网盘
async saveResourceToDrive(
resource: ResourceItem,
folderId: string,
drive: CloudDriveConfig
): Promise {
const link = resource.cloudLinks.find((link) => drive.regex.test(link));
if (!link) return;
const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match);
const shareInfo = {
...this.shareInfo,
list: this.resourceSelect.filter((x) => x.isChecked),
};
console.log(shareInfo);
const params = drive.getSaveParams({
shareInfo,
...parsedCode,
folderId,
});
const result = await drive.api.saveFile(params);
if (result.code === 0) {
ElMessage.success(`${drive.name} 转存成功`);
} else {
ElMessage.error(result.message);
}
},
// 解析云盘链接
async parsingCloudLink(url: string): Promise {
this.loading = true;
this.resources = [];
try {
const matchedDrive = CLOUD_DRIVES.find((drive) => drive.regex.test(url));
if (!matchedDrive) throw new Error("不支持的网盘链接");
const match = url.match(matchedDrive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = matchedDrive.parseShareCode(match);
const shareInfo = await matchedDrive.api.getShareInfo(parsedCode);
if (shareInfo?.list?.length) {
this.resources = [
{
id: "",
channelInfo: {
name: "自定义搜索",
channelLogo: "",
channelId: "",
},
displayList: true,
list: [
{
id: "1",
title: shareInfo.list.map((item) => item.fileName).join(", "),
cloudLinks: [url],
cloudType: matchedDrive.type,
channel: matchedDrive.name,
pubDate: "",
isSupportSave: true,
},
],
},
];
} else {
throw new Error("解析失败,请检查链接是否正确");
}
} catch (error) {
this.handleError("解析失败,请重试", error);
} finally {
this.loading = false;
}
},
// 获取资源列表并选择
async getResourceListAndSelect(resource: ResourceItem): Promise {
this.setSelectedResource([]);
const { cloudType } = resource;
const drive = CLOUD_DRIVES.find((x) => x.type === cloudType);
if (!drive) {
return false;
}
const link = resource.cloudLinks.find((link) => drive.regex.test(link));
if (!link) return false;
const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match);
this.setLoadTree(true);
let shareInfo = await drive.api.getShareInfo(parsedCode);
console.log(shareInfo);
this.setLoadTree(false);
if (shareInfo) {
shareInfo = {
...shareInfo,
...parsedCode,
};
this.shareInfo = shareInfo;
this.setSelectedResource(this.shareInfo.list.map((x) => ({ ...x, isChecked: true })));
return true;
} else {
ElMessage.error("获取资源信息失败,请先检查cookie!");
return false;
}
},
// 统一错误处理
handleError(message: string, error: unknown): void {
console.error(message, error);
ElMessage.error(error instanceof Error ? error.message : message);
},
},
});
================================================
FILE: frontend/src/stores/userSetting.ts
================================================
import { defineStore } from "pinia";
import type {
UserSettingStore,
GlobalSettingAttributes,
UserSettingAttributes,
} 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: (localStorage.getItem("display_style") as "table" | "card") || "card",
imagesSource: (localStorage.getItem("images_source") as "proxy" | "local") || "proxy",
}),
actions: {
async getSettings() {
const { data } = await settingApi.getSetting();
if (data) {
this.globalSetting = data.globalSetting;
this.userSettings = data.userSettings;
}
},
async saveSettings(settings: {
globalSetting?: GlobalSettingAttributes | null;
userSettings: UserSettingAttributes;
}) {
try {
await settingApi.saveSetting(settings);
await this.getSettings();
} catch (error) {
console.log(error);
throw error;
}
},
setDisplayStyle(style: "table" | "card") {
this.displayStyle = style;
localStorage.setItem("display_style", style);
ElMessage.success(`切换成功,当前为${style === "table" ? "列表" : "卡片"}模式`);
},
setImagesSource(source: "proxy" | "local") {
this.imagesSource = source;
localStorage.setItem("images_source", source);
ElMessage.success(`切换成功,图片模式当前为${source === "proxy" ? "代理" : "直连"}模式`);
},
},
});
================================================
FILE: frontend/src/styles/common.scss
================================================
// 颜色系统
:root {
// 主题色
--theme-primary: #0066cc;
--theme-primary-hover: #0256ac;
--theme-success: #28cd41;
--theme-warning: #ff9f0a;
--theme-error: #ff3b30;
// 中性色
--theme-bg: #f5f7fa;
--theme-card-bg: rgba(255, 255, 255, 0.8);
--theme-text-primary: #000000;
--theme-text-regular: #424242;
--theme-text-secondary: #6e6e6e;
// 特效
--theme-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--theme-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
--theme-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
// 圆角
--theme-radius-sm: 8px;
--theme-radius: 12px;
--theme-radius-lg: 16px;
// 模糊效果
--theme-blur: blur(12px);
// 动画
--theme-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
// Mixins
@mixin glass-effect {
background: var(--theme-card-bg);
backdrop-filter: var(--theme-blur);
border: 1px solid rgba(255, 255, 255, 0.2);
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 通用动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
================================================
FILE: frontend/src/styles/global.scss
================================================
:root {
--theme-color: #3e3e3e;
--theme-theme: #133ab3;
--theme-background: #fafafa;
--theme-other_background: #ffffff;
}
input {
border: none;
outline: none;
}
================================================
FILE: frontend/src/styles/mobile.scss
================================================
/* 移动端通用样式类 */
.mobile-page {
padding: var(--spacing-base);
min-height: 100vh;
background-color: var(--theme-background);
}
.mobile-card {
background: var(--theme-other_background);
border-radius: var(--border-radius-lg);
padding: var(--spacing-base);
margin-bottom: var(--spacing-base);
}
.mobile-title {
font-size: var(--font-size-xl);
font-weight: bold;
margin-bottom: var(--spacing-base);
}
.mobile-text {
font-size: var(--font-size-base);
line-height: 1.6;
color: var(--theme-color);
}
.mobile-button {
width: 100%;
height: 40px;
font-size: var(--font-size-lg);
border-radius: 20px;
}
.mobile-form {
.van-field {
padding: var(--spacing-base);
&__label {
font-size: var(--font-size-base);
}
&__control {
font-size: var(--font-size-base);
}
}
}
================================================
FILE: frontend/src/styles/responsive.scss
================================================
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 响应式布局工具类
@mixin mobile {
@media screen and (max-width: 768px) {
@content;
}
}
@mixin tablet {
@media screen and (min-width: 769px) and (max-width: 1024px) {
@content;
}
}
@mixin desktop {
@media screen and (min-width: 1025px) {
@content;
}
}
// 通用样式变量
:root {
// 字体大小 - 整体缩小约25%
--font-size-xs: 20px; // 原24px
--font-size-sm: 22px; // 原26px
--font-size-base: 24px; // 原28px
--font-size-lg: 28px; // 原32px
--font-size-xl: 32px; // 原36px
// 间距 - 也相应缩小
--spacing-xs: 6px; // 原8px
--spacing-sm: 10px; // 原12px
--spacing-base: 14px; // 原16px
--spacing-lg: 20px; // 原24px
--spacing-xl: 28px; // 原32px
// 圆角 - 适当调整
--border-radius-sm: 6px; // 原8px
--border-radius-base: 10px; // 原12px
--border-radius-lg: 14px; // 原16px
--border-radius-xl: 20px; // 原24px
// 移动端特殊变量
@include mobile {
--font-size-base: 14px;
--spacing-base: 12px;
}
}
// 移动端适配
@media screen and (max-width: 768px) {
:root {
// 间距
--spacing-xs: 3px;
--spacing-sm: 5px;
--spacing-base: 7px;
--spacing-lg: 10px;
--spacing-xl: 14px;
// 字体大小
--font-size-xs: 10px;
--font-size-sm: 11px;
--font-size-base: 12px;
--font-size-lg: 14px;
--font-size-xl: 16px;
// 圆角
--border-radius-sm: 3px;
--border-radius-base: 5px;
--border-radius-lg: 7px;
}
}
================================================
FILE: frontend/src/types/douban.ts
================================================
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;
}
================================================
FILE: frontend/src/types/globals.d.ts
================================================
declare global {
interface Location {
// 根据你的需求定义 location 的属性和方法
pathname: string;
search: string;
hash: string;
host: string;
// 其他属性和方法...
}
interface Window {
location: Location;
}
}
export {};
================================================
FILE: frontend/src/types/index.ts
================================================
export interface ResourceItem {
id: string;
title: string;
channel: string;
channelId?: string;
image?: string;
cloudLinks: string[];
tags?: string[];
content?: string;
pubDate: string;
cloudType: string;
messageId?: string;
isLastMessage?: boolean;
isSupportSave?: boolean;
}
export interface Resource {
list: ResourceItem[];
displayList?: boolean;
loading?: boolean;
channelInfo: {
channelId: string;
name: string;
channelLogo: string;
};
id: string;
}
export interface ShareInfo {
fileId: string;
fileName: string;
fileSize?: number;
fileIdToken?: string;
isChecked?: boolean;
}
export interface ShareInfoItem {
fileId: string;
fileName: string;
fileSize?: number;
fileIdToken?: string;
}
export interface ShareInfoResponse {
list: ShareInfoItem[];
fileSize?: number;
pwdId?: string;
stoken?: string;
}
export interface ShareFileInfoAndFolder {
shareInfo: ShareInfoResponse;
folderId: string;
shareCode: string;
receiveCode?: string;
}
export interface Folder {
cid: string;
name: string;
path?: Folder[];
}
export interface SaveFileParams {
shareCode: string; // 分享code
receiveCode?: string; // 分享文件的密码
folderId: string; // 文件夹id
fids: string[]; // 存储文件id
fidTokens?: string[]; // 存储文件token
}
export interface GetShareInfoParams {
shareCode: string;
receiveCode?: string;
}
export interface ApiResponse {
success: boolean;
data?: T;
error?: string;
}
export interface Save115FileParams {
shareCode: string;
receiveCode: string;
fileId: string;
folderId: string;
}
export interface SaveQuarkFileParams {
fid_list: string[];
fid_token_list: string[];
to_pdir_fid: string;
pwd_id: string;
stoken: string;
pdir_fid: 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;
}
================================================
FILE: frontend/src/types/response.ts
================================================
export type RequestErrorCode = -1 | 400 | 401 | 402 | 403 | 500 | 501;
export interface RequestSuccess {
code: 0;
data: T;
message: string;
}
export interface RequestError {
code: RequestErrorCode;
message: string;
data?: T;
}
export type RequestResult = RequestSuccess | RequestError;
================================================
FILE: frontend/src/types/user.ts
================================================
export interface GlobalSettingAttributes {
httpProxyHost: string;
httpProxyPort: string | number;
isProxyEnabled: boolean;
AdminUserCode: number;
CommonUserCode: number;
}
export interface UserSettingAttributes {
cloud115Cookie: string;
quarkCookie: string;
}
export interface UserSettingStore {
globalSetting: GlobalSettingAttributes | null;
userSettings: UserSettingAttributes;
displayStyle: "table" | "card";
imagesSource: "proxy" | "local";
}
================================================
FILE: frontend/src/utils/device.ts
================================================
export const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
};
export const isTablet = () => {
const userAgent = navigator.userAgent.toLowerCase();
return /(ipad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test(
userAgent
);
};
================================================
FILE: frontend/src/utils/image.ts
================================================
import { useUserSettingStore } from "@/stores/userSetting";
import defaultImage from "@/assets/images/default.png";
export const getProxyImageUrl = (originalUrl: string): string => {
const userStore = useUserSettingStore();
if (!originalUrl) return defaultImage;
return userStore.imagesSource === "proxy"
? `/tele-images/?url=${encodeURIComponent(originalUrl)}`
: originalUrl;
};
================================================
FILE: frontend/src/utils/index.ts
================================================
export const formattedFileSize = (size: number): string => {
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)}KB`;
}
if (size < 1024 * 1024 * 1024) {
return `${(size / 1024 / 1024).toFixed(2)}MB`;
}
return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
};
export function isMobileDevice() {
return (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768
);
}
export function throttle any>(fn: T, delay: number): T {
let lastTime = 0;
return function (this: any, ...args: Parameters) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
} as T;
}
================================================
FILE: frontend/src/utils/request.ts
================================================
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { ElMessage } from "element-plus";
import { isMobileDevice } from "@/utils/index";
import { showNotify } from "vant";
import { RequestResult } from "../types/response";
import { STORAGE_KEYS } from "@/constants/storage";
const errorMessage = (message: string) => {
if (isMobileDevice()) {
console.log(message);
showNotify({
type: "danger",
message,
});
return;
}
ElMessage.error(message);
};
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL as string,
timeout: 16000,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
function isLoginAndRedirect(url: string) {
return url.includes("/api/user/login") || url.includes("/api/user/register");
}
axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else if (!isLoginAndRedirect(config.url || "")) {
errorMessage("请先登录");
window.location.href = "/login";
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
return res;
},
(error) => {
if (error.response.status === 401) {
errorMessage("登录过期,请重新登录");
localStorage.removeItem(STORAGE_KEYS.TOKEN);
window.location.href = "/login";
return Promise.reject(new Error("登录过期,请重新登录"));
}
errorMessage(error.response.statusText);
return Promise.reject(new Error(error.response.statusText));
}
);
const request = {
get: (url: string, config?: AxiosRequestConfig): Promise> => {
return axiosInstance.get(url, { ...config });
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
post: (
url: string,
data: D,
config?: AxiosRequestConfig
): Promise> => {
return axiosInstance.post(url, data, { ...config });
},
put: axiosInstance.put,
delete: axiosInstance.delete,
};
export default request;
================================================
FILE: frontend/src/views/Douban.vue
================================================
================================================
FILE: frontend/src/views/Home.vue
================================================
================================================
FILE: frontend/src/views/ResourceList.vue
================================================
================================================
FILE: frontend/src/views/Setting.vue
================================================
帮助文档
CloudSaver部署与使用常见问题
CloudSaver功能介绍
如何获取115网盘Cookie?
如何获取夸克网盘Cookie?
保存设置
================================================
FILE: frontend/src/views/Thanks.vue
================================================
感谢Ta们对项目的赞赏
感谢每一位支持者的信任与鼓励
正是你们的支持让这个项目能够持续发展
================================================
FILE: frontend/src/views/mobile/Douban.vue
================================================
================================================
FILE: frontend/src/views/mobile/Home.vue
================================================
搜索
热门
设置
资源搜索中...
================================================
FILE: frontend/src/views/mobile/Login.vue
================================================
记住密码
{{ isLoading ? "登录中..." : "登录" }}
{{ isLoading ? "注册中..." : "注册" }}
================================================
FILE: frontend/src/views/mobile/ResourceList.vue
================================================
================================================
FILE: frontend/src/views/mobile/Setting.vue
================================================
================================================
FILE: frontend/src/views/pc/Login.vue
================================================
================================================
FILE: frontend/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/@types", "./types"],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
================================================
FILE: frontend/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: frontend/vite.config.ts
================================================
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import { VantResolver } from "@vant/auto-import-resolver";
import { VitePWA } from "vite-plugin-pwa";
import { resolve } from "path";
export default defineConfig({
base: "/",
plugins: [
vue(),
VitePWA({
registerType: "autoUpdate",
includeAssets: ["logo-1.png", "logo.svg"],
injectRegister: "auto",
workbox: {
globPatterns: ["**/*.{js,css,html,png,svg}"],
},
manifest: {
name: "CloudSaver",
short_name: "CloudSaver",
description: "网盘资源搜索工具",
theme_color: "#ffffff",
background_color: "#ffffff",
display: "standalone",
scope: "/",
start_url: "/",
icons: [
{
src: "logo-1.png",
sizes: "192x192",
type: "image/png",
},
{
src: "logo.svg",
sizes: "192x192",
type: "image/svg+xml",
},
],
},
}),
AutoImport({
resolvers: [ElementPlusResolver(), VantResolver()],
}),
Components({
resolvers: [ElementPlusResolver(), VantResolver()],
}),
],
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/global.scss";`,
},
},
},
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
server: {
host: "0.0.0.0",
port: 8008,
proxy: {
"/api": {
target: process.env.VITE_API_BASE_URL_PROXY || "http://127.0.0.1:8009",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
configure: (proxy, _options) => {
proxy.on("error", (err, _req, _res) => {
console.log("proxy error", err);
});
proxy.on("proxyReq", (proxyReq, req, _res) => {
console.log("Sending Request:", req.method, req.url);
});
proxy.on("proxyRes", (proxyRes, req, _res) => {
console.log("Received Response:", proxyRes.statusCode, req.url);
});
},
},
"/tele-images": {
target: process.env.VITE_API_BASE_URL_PROXY || "http://127.0.0.1:8009",
changeOrigin: true,
},
},
},
build: {
outDir: "dist",
assetsDir: "assets",
rollupOptions: {
input: {
main: fileURLToPath(new URL("./index.html", import.meta.url)),
},
},
},
});
================================================
FILE: nginx.conf
================================================
# nginx.conf
user root; # 定义 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;
}
}
}
================================================
FILE: package.json
================================================
{
"name": "cloud-saver",
"version": "0.2.5",
"private": true,
"workspaces": [
"frontend",
"backend"
],
"scripts": {
"ins": "npm-run-all --parallel install:*",
"install:frontend": "cd frontend && npm install",
"install:backend": "cd backend && npm install",
"dev": "npm-run-all --parallel dev:*",
"dev:frontend": "cd frontend && npm run dev",
"dev:backend": "cd backend && npm run dev",
"build": "npm-run-all --parallel build:*",
"build:frontend": "cd frontend && npm run build",
"build:backend": "cd backend && npm run build",
"clean": "rimraf **/node_modules **/dist",
"version:patch": "npm version patch -w frontend && npm version patch",
"version:minor": "npm version minor -w frontend && npm version minor",
"version:major": "npm version major -w frontend && npm version major",
"format": "prettier --write \"**/*.{js,ts,vue,json,css,scss}\"",
"format:check": "prettier --check \"**/*.{js,ts,vue,json,css,scss}\"",
"format:all": "npm run format && npm run lint:fix",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-vue": "^9.32.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"rimraf": "^5.0.5",
"vue-eslint-parser": "^9.4.3"
},
"engines": {
"pnpm": ">=6.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"bcrypt",
"esbuild",
"sqlite3",
"vue-demi"
]
}
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'frontend'
- 'backend'