[
  {
    "path": ".eslintignore",
    "content": "node_modules\ndist\nbuild\ncoverage "
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  ignorePatterns: [\"node_modules\", \"dist\", \"build\", \"coverage\"],\n  env: {\n    node: true,\n    es6: true,\n  },\n  parser: \"@typescript-eslint/parser\",\n  plugins: [\"@typescript-eslint\"],\n  extends: [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:prettier/recommended\",\n  ],\n  rules: {\n    \"prettier/prettier\": \"error\",\n    \"@typescript-eslint/no-explicit-any\": \"warn\",\n    \"@typescript-eslint/no-unused-vars\": [\"warn\", { argsIgnorePattern: \"^_\" }],\n    \"@typescript-eslint/explicit-function-return-type\": 0,\n  },\n  overrides: [\n    {\n      files: [\"frontend/**/*.{js,ts,vue}\"],\n      env: {\n        browser: true,\n      },\n      parser: \"vue-eslint-parser\",\n      parserOptions: {\n        parser: \"@typescript-eslint/parser\",\n        ecmaVersion: 2020,\n        sourceType: \"module\",\n      },\n      extends: [\n        \"eslint:recommended\",\n        \"plugin:@typescript-eslint/recommended\",\n        \"plugin:vue/vue3-recommended\",\n        \"plugin:prettier/recommended\",\n      ],\n      plugins: [\"@typescript-eslint\", \"vue\"],\n      rules: {\n        \"vue/multi-word-component-names\": \"off\",\n        \"vue/require-default-prop\": \"off\",\n        \"vue/no-v-html\": \"off\",\n      },\n    },\n    {\n      files: [\"backend/**/*.{js,ts}\"],\n      env: {\n        node: true,\n      },\n      rules: {\n        \"@typescript-eslint/explicit-function-return-type\": 0,\n        \"@typescript-eslint/no-non-null-assertion\": \"warn\",\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".github/workflows/docker-build-test.yml",
    "content": "name: Build and Push Multi-Arch Docker Image for Test\non:\n  workflow_dispatch: # 添加手动触发\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write # 必须授权以推送镜像\n    env:\n      REPO_NAME: ${{ github.repository }}\n      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}\n      IMAGE_NAME: cloudsaver\n    steps:\n      - name: 检出代码\n        uses: actions/checkout@v4\n\n      - name: 设置小写镜像名称\n        run: |\n          LOWER_NAME=$(echo \"$REPO_NAME\" | tr '[:upper:]' '[:lower:]')\n          echo \"LOWER_NAME=$LOWER_NAME\" >> $GITHUB_ENV\n\n      - name: 登录到 GitHub Container Registry\n        uses: docker/login-action@v2\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: 登录到 Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: 设置 QEMU 支持多架构\n        uses: docker/setup-qemu-action@v2\n\n      - name: 设置 Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: 构建并推送多架构 Docker 镜像\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64 # 指定架构：x86_64 和 ARM64\n          push: true\n          tags: |\n            ghcr.io/${{ env.LOWER_NAME }}:test\n            ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:test\n"
  },
  {
    "path": ".github/workflows/docker-image.yml",
    "content": "name: Docker Image CI/CD\non:\n  push:\n    tags: [\"v*.*.*\"] # 支持标签触发（如 v1.0.0）\n  workflow_dispatch: # 添加手动触发\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write # 必须授权以推送镜像\n    env:\n      REPO_NAME: ${{ github.repository }}\n      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}\n      IMAGE_NAME: cloudsaver\n    steps:\n      - name: 检出代码\n        uses: actions/checkout@v4\n\n      - name: 设置小写镜像名称和版本\n        run: |\n          LOWER_NAME=$(echo \"$REPO_NAME\" | tr '[:upper:]' '[:lower:]')\n          echo \"LOWER_NAME=$LOWER_NAME\" >> $GITHUB_ENV\n          VERSION=${GITHUB_REF#refs/tags/v}\n          echo \"VERSION=$VERSION\" >> $GITHUB_ENV\n\n      - name: 登录到 GitHub Container Registry\n        uses: docker/login-action@v2\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: 登录到 Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: 设置 QEMU 支持多架构\n        uses: docker/setup-qemu-action@v2\n\n      - name: 设置 Docker Buildx\n        uses: docker/setup-buildx-action@v2\n\n      - name: 构建并推送 Docker 镜像\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64 # 指定架构：x86_64 和 ARM64\n          push: true\n          tags: |\n            ghcr.io/${{ env.LOWER_NAME }}:latest\n            ghcr.io/${{ env.LOWER_NAME }}:${{ env.VERSION }}\n            ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest\n            ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\nlogs/\ndist/\n.env\n.env.local\n.env.*.local\n\n*.tar\n\n# 数据库数据\n*.sqlite\n\n# 保留模板\n!.env.example\n\n!frontend/.env\n\n# 其他敏感文件\nconfig.private.ts\n*.pem\n*.key\n\n.DS_Store\n*.log "
  },
  {
    "path": ".prettierignore",
    "content": "# 构建产物\ndist\nbuild\ncoverage\n\n# 依赖目录\nnode_modules\n\n# 日志文件\n*.log\n\n# 环境配置\n.env*\n!.env.example\n\n# 编辑器配置\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# 系统文件\n.DS_Store\nThumbs.db\n\n# 版本控制\n.git "
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  semi: true,\n  trailingComma: \"es5\",\n  singleQuote: false,\n  printWidth: 100,\n  tabWidth: 2,\n  useTabs: false,\n  endOfLine: \"auto\",\n  arrowParens: \"always\",\n  bracketSpacing: true,\n  embeddedLanguageFormatting: \"auto\",\n  htmlWhitespaceSensitivity: \"css\",\n  vueIndentScriptAndStyle: false,\n};\n"
  },
  {
    "path": "Dockerfile",
    "content": "# 构建前端项目\nFROM node:18-alpine as frontend-build\nWORKDIR /app\nCOPY frontend/package*.json ./\nRUN npm install -g pnpm\nRUN pnpm install\nCOPY frontend/ ./\nRUN npm run build\n\n# 构建后端项目\nFROM node:18-alpine as backend-build\nWORKDIR /app\nCOPY backend/package*.json ./\nRUN npm install -g pnpm\nRUN pnpm install\nCOPY backend/ ./\nRUN rm -f database.sqlite\nRUN npm run build\n\n# 生产环境镜像\nFROM node:18-alpine\n\n# 安装 Nginx\nRUN apk add --no-cache nginx\n\n# 设置工作目录\nWORKDIR /app\n\n# 创建配置和数据目录\nRUN mkdir -p /app/config /app/data\n\n# 复制前端构建产物到 Nginx\nCOPY --from=frontend-build /app/dist /usr/share/nginx/html\n\n# 复制 Nginx 配置文件\nCOPY nginx.conf /etc/nginx/nginx.conf\n\n# 复制后端构建产物到生产环境镜像\nCOPY --from=backend-build /app /app\n\n# 安装生产环境依赖\nRUN npm install --production\n\n# 设置数据卷\nVOLUME [\"/app/config\", \"/app/data\"]\n\n# 暴露端口\nEXPOSE 8008\n\n# 启动脚本\nCOPY docker-entrypoint.sh /app/\nRUN chmod +x /app/docker-entrypoint.sh\n\n# 启动服务\nENTRYPOINT [\"/app/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 CloudSaver\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# CloudSaver\n\n![License](https://img.shields.io/badge/license-MIT-blue.svg)\n![Vue](https://img.shields.io/badge/vue-3.x-brightgreen.svg)\n![TypeScript](https://img.shields.io/badge/typescript-5.x-blue.svg)\n[![GitHub Stars](https://img.shields.io/github/stars/jiangrui1994/CloudSaver.svg?style=flat&logo=github)](https://github.com/jiangrui1994/CloudSaver/stargazers)\n![Docker](https://img.shields.io/docker/pulls/jiangrui1994/cloudsaver.svg)\n<a href=\"https://hellogithub.com/repository/d13663fb959345e7923ecaccc3387571\" target=\"_blank\"><img src=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d13663fb959345e7923ecaccc3387571&claim_uid=xP1MT4mSvN6wn5K&theme=small\" alt=\"Featured｜HelloGitHub\" /></a>\n\n一个基于 Vue 3 + Express 的网盘资源搜索与转存工具，支持响应式布局，移动端与PC完美适配，可通过 Docker 一键部署。\n\n官方Telegram群组：[https://t.me/+4fWSSbQn5rMxYjM1](https://t.me/+4fWSSbQn5rMxYjM1)\n\n官方QQ交流群（[群地址](https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l#lsPla))\n\n版本更新日志：[https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)\n\nCloudSaver部署与使用常见问题(包含更多搜索频道)：[https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l](https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l)\n密码 me16 点个Star呗~\n\n⚠️关于项目更新与需求处理的核心声明：[https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx](https://www.yuque.com/xiaoruihenbangde/ggogn3/gt9cgqn2n3vergxx)\n\n---\n\n⚠️**由于某些原因，[新版本](https://www.yuque.com/xiaoruihenbangde/ggogn3/vxoqxkx4rkcz3g94)内容不包含在此开源仓库(停留在V0.2.5版本)，如需使用，请使用docker镜像进行部署使用。**\n\n---\n\n**🔒 重要安全提醒｜关于本项目私有化部署的强制建议**\n\n为保障您的数据安全与隐私权益，请务必**通过Docker自行私有化部署本项目**。我们**强烈反对**使用任何第三方提供的在线网站或他人部署的服务，原因如下：\n\n⚠️ **高风险预警**\n\n- 本项目涉及**网盘Cookie等敏感凭据**，若使用他人服务：  \n  ▶ 您的账号密码、隐私文件可能遭恶意窃取或篡改  \n  ▶ 攻击者可利用Cookie直接登录您的网盘实施破坏  \n  ▶ 数据泄露、资产损失等后果需完全由使用者自行承担\n\n🚫 **严正声明**\n\n1. 本项目**从未且不会**提供任何形式的在线服务、公开Demo或托管平台\n2. **任何声称与本项目相关的在线网站均为未授权第三方搭建**，存在蓄意作恶的高风险\n3. 如因使用非自建服务导致损失，本项目开发者**不承担任何法律责任**\n\n❓ **常见问题**  \nQ: 是否有在线Demo可直接试用？  \nA: **绝无可能！** 任何在线服务都与本项目无关，请立即关闭避免信息泄露\n\nQ: 为何不能使用他人部署好的服务？  \nA: Cookie等同于账号密码，交出Cookie=交出家门钥匙，请勿将身家安全托付陌生人\n\nQ: 如何确保100%安全？  \nA: 唯一可信方案：通过官方仓库代码+自主服务器部署，全程数据闭环\n\n---\n\n**🛡️ 最后一次严肃提醒**  \n您的账号安全只应掌握在自己手中！  \n请立即执行私有化部署 ▶ 避免无法挽回的数据灾难\n\n---\n\n## 功能特性\n\n- 🔍 多源资源搜索\n  - 支持多个资源订阅源搜索\n  - 支持关键词搜索与资源链接解析\n  - 支持豆瓣热门榜单展示\n- 💾 网盘资源转存\n  - 支持**115 网盘，夸克网盘，天翼网盘，123云盘**一键转存\n  - 支持转存文件夹展示与选择\n- 👥 多用户系统\n  - 支持用户注册登录\n  - 支持管理员与普通用户权限区分\n- 📱 响应式设计\n  - 支持 PC 端与移动端自适应布局\n  - 针对不同设备优化的交互体验\n\n## 产品展示\n\n<details>\n<summary>点击展开截图预览</summary>\n\n### PC 端\n\n<div align=\"center\">\n  <img src=\"./docs/images/pc/login.png\" width=\"400\" alt=\"PC登录页面\">\n   <img src=\"./docs/images/pc/douban.png\" width=\"400\" alt=\"PC豆瓣榜单\">\n  <p>登录页面/榜单</p>\n</div>\n\n<div align=\"center\">\n  <img src=\"./docs/images/pc/search.png\" width=\"400\" alt=\"PC资源搜索\">\n  <img src=\"./docs/images/pc/detail.png\" width=\"400\" alt=\"PC资源详情\">\n  <p>资源搜索/资源详情</p>\n</div>\n\n<div align=\"center\">\n  <img src=\"./docs/images/pc/save.png\" width=\"400\" alt=\"PC资源转存\">\n  <img src=\"./docs/images/pc/save1.png\" width=\"400\" alt=\"PC资源转存\">\n  <p>资源转存</p>\n</div>\n\n### 移动端\n\n<div align=\"center\">\n  <div style=\"display: inline-block; margin: 0 20px;\">\n    <img src=\"./docs/images/mobile/login.png\" width=\"200\" alt=\"移动端登录页面\">\n    <img src=\"./docs/images/mobile/search.png\" width=\"200\" alt=\"移动端资源搜索\">\n    <img src=\"./docs/images/mobile/save.png\" width=\"200\" alt=\"移动端资源转存\">\n    <img src=\"./docs/images/mobile/save1.png\" width=\"200\" alt=\"移动端资源转存\">\n  </div>\n</div>\n\n</details>\n\n## 技术栈\n\n### 前端\n\n- 核心框架\n  - Vue 3\n  - TypeScript\n  - Vite\n- 状态管理\n  - Pinia\n- 路由管理\n  - Vue Router\n- UI 组件库\n  - Element Plus (PC)\n  - Vant (Mobile)\n- 工具库\n  - Axios\n\n### 后端\n\n- 运行环境\n  - Node.js\n  - Express\n- 数据存储\n  - SQLite3\n\n## 环境要求\n\n- Node.js >= 18.x\n- pnpm >= 8.x (推荐)\n\n## 快速开始\n\n### 开发环境\n\n1. 克隆项目\n\n```bash\ngit clone https://github.com/jiangrui1994/CloudSaver.git\ncd CloudSaver\n```\n\n2. 安装依赖\n\n```bash\npnpm install\n```\n\n3. 配置环境变量\n\n```bash\ncp ./backend/.env.example ./backend/.env\n```\n\n根据 `.env.example` 文件说明配置必要的环境变量。\n\n4. 启动开发服务器\n\n```bash\npnpm dev\n```\n\n### 生产环境部署\n\n1. 构建前端\n\n```bash\npnpm build:frontend\n```\n\n2. 构建后端\n\n```bash\ncd backend\npnpm build\n```\n\n3. 启动服务\n\n```bash\npnpm start\n```\n\n### Docker 部署\n\n说明：镜像源有**两个地址**供选择，下面部署命令中使用的是dockerhub托管的地址为例，github托管的地址请自行替换\n\n- dockerhub托管：\n  - `jiangrui1994/cloudsaver:latest` 稳定版\n  - `jiangrui1994/cloudsaver:test` 测试版 （包含最新功能和bug修复，但可能不如稳定版稳定）\n- github托管：\n  - `ghcr.io/jiangrui1994/cloudsaver:latest` 稳定版\n  - `ghcr.io/jiangrui1994/cloudsaver:test` 测试版 （包含最新功能和bug修复，但可能不如稳定版稳定）\n\n#### 单容器部署\n\n稳定版：\n\n```bash\ndocker run -d \\\n  -p 8008:8008 \\\n  -v /your/local/path/data:/app/data \\\n  -v /your/local/path/config:/app/config \\\n  --name cloud-saver \\\n  jiangrui1994/cloudsaver:latest\n```\n\n测试版（包含最新功能和bug修复，但可能不如稳定版稳定）：\n\n```bash\ndocker run -d \\\n  -p 8008:8008 \\\n  -v /your/local/path/data:/app/data \\\n  -v /your/local/path/config:/app/config \\\n  --name cloud-saver \\\n  jiangrui1994/cloudsaver:test\n```\n\n#### Docker Compose 部署\n\n创建 `docker-compose.yml` 文件：\n\n稳定版：\n\n```yaml\nversion: \"3\"\nservices:\n  cloudsaver:\n    image: jiangrui1994/cloudsaver:latest\n    container_name: cloud-saver\n    ports:\n      - \"8008:8008\"\n    volumes:\n      - /your/local/path/data:/app/data\n      - /your/local/path/config:/app/config\n    restart: unless-stopped\n```\n\n测试版：\n\n```yaml\nversion: \"3\"\nservices:\n  cloudsaver:\n    image: jiangrui1994/cloudsaver:test\n    container_name: cloud-saver\n    ports:\n      - \"8008:8008\"\n    volumes:\n      - /your/local/path/data:/app/data\n      - /your/local/path/config:/app/config\n    restart: unless-stopped\n```\n\n#### /app/config 目录说明\n\n- `env` 文件：包含后端环境变量配置\n\n```bash\n# JWT配置\nJWT_SECRET=your_jwt_secret_here\n\n# Telegram配置\nTELEGRAM_BASE_URL=https://t.me/s\n\n# Telegram频道配置(0.3.0及之后版本无效)\nTELE_CHANNELS=[{\"id\":\"xxxx\",\"name\":\"xxxx资源分享\"}]\n```\n\n运行：\n\n```bash\ndocker-compose up -d\n```\n\n> **注意**: 测试版（:test标签）包含最新的功能开发和bug修复，但可能存在不稳定因素。建议生产环境使用稳定版（:latest标签）。\n\n## 注意事项\n\n1. 资源搜索需要配置代理环境\n2. 默认注册码\n   - 管理员：230713\n   - 普通用户：9527\n\n## 联系方式\n\n<div align=\"center\">\n  <div>\n   官方QQ交流群（群地址(https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l#lsPla))\n  </div>\n</div>\n\n## 支持项目\n\n如果您觉得这个项目对您有帮助，可以考虑给予一点支持，这将帮助我们持续改进项目 ❤️\n\n您可以：\n\n- ⭐ 给项目点个 Star\n- 🎉 分享给更多有需要的朋友\n- ☕ 请作者喝杯冰阔乐或咖啡\n- 💰 **赞赏了一定记得和我联系**\n\n<div align=\"center\">\n  <div style=\"display: inline-block; margin: 0 20px;\">\n    <img src=\"./docs/images/wechat_pay.jpg\" height=\"300\" alt=\"微信打赏\">\n    <img src=\"./docs/images/alipay.png\" height=\"300\" alt=\"支付宝打赏\">\n  </div>\n</div>\n\n## 特别声明\n\n1. 本项目仅供学习交流使用，请勿用于非法用途\n2. 仅支持个人使用，不支持任何形式的商业使用\n3. 禁止在项目页面进行任何形式的广告宣传\n4. 所有搜索到的资源均来自第三方，本项目不对其真实性、合法性做出任何保证\n\n## 贡献指南\n\n1. Fork 本仓库\n2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)\n3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)\n4. 推送到分支 (`git push origin feature/AmazingFeature`)\n5. 提交 Pull Request\n\n## 开源协议\n\n本项目基于 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解更多细节\n\n## 鸣谢\n\n- 👨‍💻 感谢所有为这个项目做出贡献的开发者们！\n- 👥 感谢所有使用本项目并提供反馈的用户！\n- 感谢所有给予支持和鼓励的朋友们！\n\n## 赞助\n本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助\n\n<a href=\"https://edgeone.ai/?from=github\" target=\"_blank\">亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne</a>\n\n<img title=\"亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne\" src=\"https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png\" width=\"300\">\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=jiangrui1994/cloudsaver&type=Date)](https://www.star-history.com/#jiangrui1994/cloudsaver&Date)\n"
  },
  {
    "path": "backend/.env.example",
    "content": "# JWT配置\nJWT_SECRET=your_jwt_secret_here\n\n# Telegram配置\nTELEGRAM_BASE_URL=https://t.me/s\n\n# Telegram频道配置\nTELE_CHANNELS=[]\n\n"
  },
  {
    "path": "backend/package.json",
    "content": "{\n  \"name\": \"cloud-saver-server\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"nodemon --exec ts-node src/app.ts\",\n    \"build\": \"tsc\",\n    \"start\": \"node dist/app.js\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.6.7\",\n    \"bcrypt\": \"^5.1.1\",\n    \"cheerio\": \"^1.0.0\",\n    \"cookie-parser\": \"^1.4.6\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.4.5\",\n    \"express\": \"^4.18.3\",\n    \"inversify\": \"^7.1.0\",\n    \"jsonwebtoken\": \"^9.0.2\",\n    \"rss-parser\": \"^3.13.0\",\n    \"sequelize\": \"^6.37.5\",\n    \"socket.io\": \"^4.8.1\",\n    \"sqlite3\": \"^5.1.7\",\n    \"tunnel\": \"^0.0.6\",\n    \"winston\": \"^3.17.0\"\n  },\n  \"devDependencies\": {\n    \"@types/bcrypt\": \"^5.0.2\",\n    \"@types/cookie-parser\": \"^1.4.7\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/express\": \"^4.17.21\",\n    \"@types/jsonwebtoken\": \"^9.0.7\",\n    \"@types/node\": \"^20.11.25\",\n    \"@types/tunnel\": \"^0.0.7\",\n    \"nodemon\": \"^3.1.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.4.2\"\n  }\n}\n"
  },
  {
    "path": "backend/src/app.ts",
    "content": "// filepath: /d:/code/CloudDiskDown/backend/src/app.ts\nimport \"./types/express\";\nimport express from \"express\";\nimport { container } from \"./inversify.config\";\nimport { TYPES } from \"./core/types\";\nimport { DatabaseService } from \"./services/DatabaseService\";\nimport { setupMiddlewares } from \"./middleware\";\nimport routes from \"./routes/api\";\nimport { logger } from \"./utils/logger\";\nimport { errorHandler } from \"./middleware/errorHandler\";\nclass App {\n  private app = express();\n  private databaseService = container.get<DatabaseService>(TYPES.DatabaseService);\n\n  constructor() {\n    this.setupExpress();\n  }\n\n  private setupExpress(): void {\n    // 设置中间件\n    setupMiddlewares(this.app);\n\n    // 设置路由\n    this.app.use(\"/\", routes);\n    this.app.use(errorHandler);\n  }\n\n  public async start(): Promise<void> {\n    try {\n      // 初始化数据库\n      await this.databaseService.initialize();\n      logger.info(\"数据库初始化成功\");\n\n      // 启动服务器\n      const port = process.env.PORT || 8009;\n      this.app.listen(port, () => {\n        logger.info(`\n🚀 服务器启动成功\n🌍 监听端口: ${port}\n🔧 运行环境: ${process.env.NODE_ENV || \"development\"}\n        `);\n      });\n    } catch (error) {\n      logger.error(\"服务器启动失败:\", error);\n      process.exit(1);\n    }\n  }\n}\n\n// 创建并启动应用\nconst application = new App();\napplication.start().catch((error) => {\n  logger.error(\"应用程序启动失败:\", error);\n  process.exit(1);\n});\n\nexport default application;\n"
  },
  {
    "path": "backend/src/config/database.ts",
    "content": "// backend/src/config/database.ts\nimport { Sequelize } from \"sequelize\";\n\nconst sequelize = new Sequelize({\n  dialect: \"sqlite\",\n  storage: \"./data/database.sqlite\",\n});\n\nexport default sequelize;\n"
  },
  {
    "path": "backend/src/config/index.ts",
    "content": "import dotenv from \"dotenv\";\n\n// 加载.env文件\ndotenv.config();\n\ninterface Channel {\n  id: string;\n  name: string;\n}\n\ninterface CloudPatterns {\n  baiduPan: RegExp;\n  tianyi: RegExp;\n  aliyun: RegExp;\n  pan115: RegExp;\n  pan123: RegExp;\n  quark: RegExp;\n  yidong: RegExp;\n}\n\ninterface Config {\n  jwtSecret: string;\n  telegram: {\n    baseUrl: string;\n    channels: Channel[];\n  };\n  cloudPatterns: CloudPatterns;\n  app: {\n    port: number;\n    env: string;\n  };\n  database: {\n    type: string;\n    path: string;\n  };\n  jwt: {\n    secret: string;\n    expiresIn: string;\n  };\n}\n\n// 从环境变量读取频道配置\nconst getTeleChannels = (): Channel[] => {\n  try {\n    const channelsStr = process.env.TELE_CHANNELS;\n    if (channelsStr) {\n      return JSON.parse(channelsStr);\n    }\n  } catch (error) {\n    console.warn(\"无法解析 TELE_CHANNELS 环境变量，使用默认配置\");\n  }\n\n  // 默认配置\n  return [];\n};\n\nexport const config: Config = {\n  app: {\n    port: parseInt(process.env.PORT || \"8009\"),\n    env: process.env.NODE_ENV || \"development\",\n  },\n  database: {\n    type: \"sqlite\",\n    path: \"./data/database.sqlite\",\n  },\n  jwt: {\n    secret: process.env.JWT_SECRET || \"your-secret-key\",\n    expiresIn: \"6h\",\n  },\n  jwtSecret: process.env.JWT_SECRET || \"uV7Y$k92#LkF^q1b!\",\n\n  telegram: {\n    baseUrl: process.env.TELEGRAM_BASE_URL || \"https://t.me/s\",\n    channels: getTeleChannels(),\n  },\n  cloudPatterns: {\n    baiduPan: /https?:\\/\\/(?:pan|yun)\\.baidu\\.com\\/[^\\s<>\"]+/g,\n    tianyi: /https?:\\/\\/cloud\\.189\\.cn\\/[^\\s<>\"]+/g,\n    aliyun: /https?:\\/\\/\\w+\\.(?:alipan|aliyundrive)\\.com\\/[^\\s<>\"]+/g,\n    // pan115有两个域名 115.com 和 anxia.com 和 115cdn.com\n    pan115: /https?:\\/\\/(?:115|anxia|115cdn)\\.com\\/s\\/[^\\s<>\"]+/g,\n    // 修改为匹配所有以123开头的域名\n    // eslint-disable-next-line no-useless-escape\n    pan123: /https?:\\/\\/(?:www\\.)?123[^\\/\\s<>\"]+\\.com\\/s\\/[^\\s<>\"]+/g,\n    quark: /https?:\\/\\/pan\\.quark\\.cn\\/[^\\s<>\"]+/g,\n    yidong: /https?:\\/\\/caiyun\\.139\\.com\\/[^\\s<>\"]+/g,\n  },\n};\n"
  },
  {
    "path": "backend/src/controllers/BaseCloudController.ts",
    "content": "import { Request, Response } from \"express\";\nimport { BaseController } from \"./BaseController\";\nimport { ICloudStorageService } from \"@/types/services\";\n\nexport abstract class BaseCloudController extends BaseController {\n  constructor(protected cloudService: ICloudStorageService) {\n    super();\n  }\n\n  async getShareInfo(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { shareCode, receiveCode } = req.query;\n      // await this.cloudService.setCookie(req);\n      return await this.cloudService.getShareInfo(shareCode as string, receiveCode as string);\n    });\n  }\n\n  async getFolderList(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { parentCid } = req.query;\n      await this.cloudService.setCookie(req);\n      return await this.cloudService.getFolderList(parentCid as string);\n    });\n  }\n\n  async saveFile(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      await this.cloudService.setCookie(req);\n      return await this.cloudService.saveSharedFile(req.body);\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/BaseController.ts",
    "content": "import { Request, Response } from \"express\";\nimport { ApiResponse } from \"../core/ApiResponse\";\ninterface ApiResponseData<T> {\n  data?: T;\n  message?: string;\n}\n\nexport abstract class BaseController {\n  protected async handleRequest<T>(\n    req: Request,\n    res: Response,\n    action: () => Promise<ApiResponseData<T> | void>\n  ): Promise<void> {\n    try {\n      const result = await action();\n      if (result) {\n        res.json(ApiResponse.success(result.data, result.message));\n      }\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : \"未知错误\";\n      res.status(200).json(ApiResponse.error(errorMessage));\n    }\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/cloud115.ts",
    "content": "import { Cloud115Service } from \"../services/Cloud115Service\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { BaseCloudController } from \"./BaseCloudController\";\n\n@injectable()\nexport class Cloud115Controller extends BaseCloudController {\n  constructor(@inject(TYPES.Cloud115Service) cloud115Service: Cloud115Service) {\n    super(cloud115Service);\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/douban.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { DoubanService } from \"../services/DoubanService\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class DoubanController extends BaseController {\n  constructor(@inject(TYPES.DoubanService) private doubanService: DoubanService) {\n    super();\n  }\n\n  async getDoubanHotList(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { type = \"movie\", tag = \"热门\", page_limit = \"50\", page_start = \"0\" } = req.query;\n      const result = await this.doubanService.getHotList({\n        type: type as string,\n        tag: tag as string,\n        page_limit: page_limit as string,\n        page_start: page_start as string,\n      });\n      return result;\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/quark.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { QuarkService } from \"../services/QuarkService\";\nimport { BaseCloudController } from \"./BaseCloudController\";\n\n@injectable()\nexport class QuarkController extends BaseCloudController {\n  constructor(@inject(TYPES.QuarkService) quarkService: QuarkService) {\n    super(quarkService);\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/resource.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { Searcher } from \"../services/Searcher\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class ResourceController extends BaseController {\n  constructor(@inject(TYPES.Searcher) private searcher: Searcher) {\n    super();\n  }\n\n  async search(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { keyword, channelId = \"\", lastMessageId = \"\" } = req.query;\n      return await this.searcher.searchAll(\n        keyword as string,\n        channelId as string,\n        lastMessageId as string\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/setting.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { SettingService } from \"../services/SettingService\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class SettingController extends BaseController {\n  constructor(@inject(TYPES.SettingService) private settingService: SettingService) {\n    super();\n  }\n\n  async get(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const userId = req.user?.userId;\n      const role = Number(req.user?.role);\n      return await this.settingService.getSettings(userId, role);\n    });\n  }\n\n  async save(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const userId = req.user?.userId;\n      const role = Number(req.user?.role);\n      return await this.settingService.saveSettings(userId, role, req.body);\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/sponsors.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { SponsorsService } from \"../services/SponsorsService\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class SponsorsController extends BaseController {\n  constructor(@inject(TYPES.SponsorsService) private sponsorsService: SponsorsService) {\n    super();\n  }\n\n  async get(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      return await this.sponsorsService.getSponsors();\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/teleImages.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { ImageService } from \"../services/ImageService\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class ImageController extends BaseController {\n  constructor(@inject(TYPES.ImageService) private imageService: ImageService) {\n    super();\n  }\n\n  async getImages(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const url = decodeURIComponent((req.query.url as string) || \"\");\n      const response = await this.imageService.getImages(url);\n\n      // 设置正确的响应头\n      res.setHeader(\"Content-Type\", response.headers[\"content-type\"]);\n      res.setHeader(\"Cache-Control\", \"no-cache\");\n\n      // 确保清除任何可能导致304响应的头信息\n      res.removeHeader(\"etag\");\n      res.removeHeader(\"last-modified\");\n\n      // 直接传输图片数据\n      response.data.pipe(res);\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/controllers/user.ts",
    "content": "import { Request, Response } from \"express\";\nimport { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport { UserService } from \"../services/UserService\";\nimport { BaseController } from \"./BaseController\";\n\n@injectable()\nexport class UserController extends BaseController {\n  constructor(@inject(TYPES.UserService) private userService: UserService) {\n    super();\n  }\n\n  async register(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { username, password, registerCode } = req.body;\n      return await this.userService.register(username, password, registerCode);\n    });\n  }\n\n  async login(req: Request, res: Response): Promise<void> {\n    await this.handleRequest(req, res, async () => {\n      const { username, password } = req.body;\n      return await this.userService.login(username, password);\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/core/ApiResponse.ts",
    "content": "export class ApiResponse<T> {\n  success: boolean;\n  data?: T;\n  message?: string;\n  code: number;\n\n  private constructor(success: boolean, code: number, data?: T, message?: string) {\n    this.success = success;\n    this.code = code;\n    this.data = data;\n    this.message = message;\n  }\n\n  static success<T>(data?: T, message = \"操作成功\"): ApiResponse<T> {\n    return new ApiResponse(true, 0, data, message);\n  }\n\n  static error(message: string, code = 10000): ApiResponse<null> {\n    return new ApiResponse(false, code, null, message);\n  }\n}\n"
  },
  {
    "path": "backend/src/core/types.ts",
    "content": "export const TYPES = {\n  DatabaseService: Symbol.for(\"DatabaseService\"),\n  Cloud115Service: Symbol.for(\"Cloud115Service\"),\n  QuarkService: Symbol.for(\"QuarkService\"),\n  Searcher: Symbol.for(\"Searcher\"),\n  DoubanService: Symbol.for(\"DoubanService\"),\n  ImageService: Symbol.for(\"ImageService\"),\n  SettingService: Symbol.for(\"SettingService\"),\n  UserService: Symbol.for(\"UserService\"),\n  SponsorsService: Symbol.for(\"SponsorsService\"),\n\n  Cloud115Controller: Symbol.for(\"Cloud115Controller\"),\n  QuarkController: Symbol.for(\"QuarkController\"),\n  ResourceController: Symbol.for(\"ResourceController\"),\n  DoubanController: Symbol.for(\"DoubanController\"),\n  ImageController: Symbol.for(\"ImageController\"),\n  SettingController: Symbol.for(\"SettingController\"),\n  UserController: Symbol.for(\"UserController\"),\n  SponsorsController: Symbol.for(\"SponsorsController\"),\n};\n"
  },
  {
    "path": "backend/src/inversify.config.ts",
    "content": "import { Container } from \"inversify\";\nimport { TYPES } from \"./core/types\";\n\n// Services\nimport { DatabaseService } from \"./services/DatabaseService\";\nimport { Cloud115Service } from \"./services/Cloud115Service\";\nimport { QuarkService } from \"./services/QuarkService\";\nimport { Searcher } from \"./services/Searcher\";\nimport { DoubanService } from \"./services/DoubanService\";\nimport { UserService } from \"./services/UserService\";\nimport { ImageService } from \"./services/ImageService\";\nimport { SettingService } from \"./services/SettingService\";\nimport { SponsorsService } from \"./services/SponsorsService\";\n// Controllers\nimport { Cloud115Controller } from \"./controllers/cloud115\";\nimport { QuarkController } from \"./controllers/quark\";\nimport { ResourceController } from \"./controllers/resource\";\nimport { DoubanController } from \"./controllers/douban\";\nimport { ImageController } from \"./controllers/teleImages\";\nimport { SettingController } from \"./controllers/setting\";\nimport { UserController } from \"./controllers/user\";\nimport { SponsorsController } from \"./controllers/sponsors\";\nconst container = new Container();\n\n// Services\ncontainer.bind<DatabaseService>(TYPES.DatabaseService).to(DatabaseService).inSingletonScope();\ncontainer.bind<Cloud115Service>(TYPES.Cloud115Service).to(Cloud115Service).inSingletonScope();\ncontainer.bind<QuarkService>(TYPES.QuarkService).to(QuarkService).inSingletonScope();\ncontainer.bind<Searcher>(TYPES.Searcher).to(Searcher).inSingletonScope();\ncontainer.bind<ImageService>(TYPES.ImageService).to(ImageService).inSingletonScope();\ncontainer.bind<SettingService>(TYPES.SettingService).to(SettingService).inSingletonScope();\ncontainer.bind<DoubanService>(TYPES.DoubanService).to(DoubanService).inSingletonScope();\ncontainer.bind<UserService>(TYPES.UserService).to(UserService).inSingletonScope();\ncontainer.bind<SponsorsService>(TYPES.SponsorsService).to(SponsorsService).inSingletonScope();\n// Controllers\ncontainer.bind<Cloud115Controller>(TYPES.Cloud115Controller).to(Cloud115Controller);\ncontainer.bind<QuarkController>(TYPES.QuarkController).to(QuarkController);\ncontainer.bind<ResourceController>(TYPES.ResourceController).to(ResourceController);\ncontainer.bind<DoubanController>(TYPES.DoubanController).to(DoubanController);\ncontainer.bind<ImageController>(TYPES.ImageController).to(ImageController);\ncontainer.bind<SettingController>(TYPES.SettingController).to(SettingController);\ncontainer.bind<UserController>(TYPES.UserController).to(UserController);\ncontainer.bind<SponsorsController>(TYPES.SponsorsController).to(SponsorsController);\n\nexport { container };\n"
  },
  {
    "path": "backend/src/middleware/auth.ts",
    "content": "// filepath: /D:/code/CloudDiskDown/backend/src/middleware/auth.ts\nimport { Request, Response, NextFunction } from \"express\";\nimport jwt, { JwtPayload } from \"jsonwebtoken\";\nimport User from \"../models/User\";\nimport { config } from \"../config\";\n\ninterface AuthenticatedRequest extends Request {\n  user?: {\n    userId: string;\n    role: number;\n  };\n}\n\nexport const authMiddleware = async (\n  req: AuthenticatedRequest,\n  res: Response,\n  next: NextFunction\n): Promise<void | Response> => {\n  if (req.path === \"/user/login\" || req.path === \"/user/register\" || req.path === \"/tele-images/\") {\n    return next();\n  }\n\n  const token = req.headers.authorization?.split(\" \")[1];\n  if (!token) {\n    return res.status(401).json({ message: \"未提供 token\" });\n  }\n\n  try {\n    const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;\n\n    req.user = {\n      userId: decoded.userId,\n      role: decoded.role,\n    };\n    const user = await User.findOne({ where: { userId: decoded.userId } });\n    if (!user) {\n      return res.status(401).json({ message: \"无效的 token\" });\n    }\n    next();\n  } catch (error) {\n    res.status(401).json({ message: \"无效的 token\" });\n  }\n};\n"
  },
  {
    "path": "backend/src/middleware/cors.ts",
    "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport const cors = () => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    res.header(\"Access-Control-Allow-Origin\", \"*\");\n    res.header(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n    res.header(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization, Cookie\");\n    res.header(\"Access-Control-Allow-Credentials\", \"true\");\n\n    if (req.method === \"OPTIONS\") {\n      return res.sendStatus(200);\n    }\n    next();\n  };\n};\n"
  },
  {
    "path": "backend/src/middleware/errorHandler.ts",
    "content": "import { Request, Response } from \"express\";\n\ninterface CustomError extends Error {\n  status?: number;\n}\n\nexport const errorHandler = (err: CustomError, req: Request, res: Response): void => {\n  res.status(err.status || 500).json({\n    success: false,\n    error: err.message || \"服务器内部错误\",\n  });\n};\n"
  },
  {
    "path": "backend/src/middleware/index.ts",
    "content": "import { Application } from \"express\";\nimport express from \"express\";\nimport { authMiddleware } from \"./auth\";\nimport { requestLogger } from \"./requestLogger\";\nimport { rateLimiter } from \"./rateLimiter\";\nimport { cors } from \"./cors\";\n\nexport const setupMiddlewares = (app: Application) => {\n  app.use(express.json());\n  app.use(cors());\n  app.use(requestLogger());\n  app.use(rateLimiter());\n  app.use(authMiddleware);\n};\n"
  },
  {
    "path": "backend/src/middleware/rateLimiter.ts",
    "content": "import { Request, Response, NextFunction } from \"express\";\n\nconst requestCounts = new Map<string, { count: number; timestamp: number }>();\nconst WINDOW_MS = 60 * 1000; // 1分钟窗口\nconst MAX_REQUESTS = 600; // 每个IP每分钟最多60个请求\n\nexport const rateLimiter = () => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const ip = req.ip || req.socket.remoteAddress || \"unknown\";\n    const now = Date.now();\n    const record = requestCounts.get(ip) || { count: 0, timestamp: now };\n\n    if (now - record.timestamp > WINDOW_MS) {\n      record.count = 0;\n      record.timestamp = now;\n    }\n\n    record.count++;\n    requestCounts.set(ip, record);\n\n    if (record.count > MAX_REQUESTS) {\n      return res.status(429).json({ message: \"请求过于频繁，请稍后再试\" });\n    }\n\n    next();\n  };\n};\n"
  },
  {
    "path": "backend/src/middleware/requestLogger.ts",
    "content": "import { Request, Response, NextFunction } from \"express\";\nimport { logger } from \"../utils/logger\";\n\nconst excludePaths = [\"/tele-images/\"];\n\nexport const requestLogger = () => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const start = Date.now();\n    res.on(\"finish\", () => {\n      if (excludePaths.includes(req.path)) {\n        return;\n      }\n      const duration = Date.now() - start;\n      logger.info({\n        method: req.method,\n        path: req.path,\n        status: res.statusCode,\n        duration: `${duration}ms`,\n      });\n    });\n    next();\n  };\n};\n"
  },
  {
    "path": "backend/src/middleware/validateRequest.ts",
    "content": "import { Request, Response, NextFunction } from \"express\";\n\nexport const validateRequest = (\n  requiredParams: string[]\n): ((req: Request, res: Response, next: NextFunction) => Response | void) => {\n  return (req: Request, res: Response, next: NextFunction) => {\n    const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);\n    if (missingParams.length > 0) {\n      return res.status(400).json({\n        success: false,\n        error: `缺少必要的参数: ${missingParams.join(\", \")}`,\n      });\n    }\n    next();\n  };\n};\n"
  },
  {
    "path": "backend/src/models/GlobalSetting.ts",
    "content": "import { DataTypes, Model, Optional } from \"sequelize\";\nimport sequelize from \"../config/database\";\n\nexport interface GlobalSettingAttributes {\n  id: number;\n  httpProxyHost: string;\n  httpProxyPort: number;\n  isProxyEnabled: boolean;\n  CommonUserCode: number;\n  AdminUserCode: number;\n}\n\ninterface GlobalSettingCreationAttributes extends Optional<GlobalSettingAttributes, \"id\"> {}\n\nclass GlobalSetting\n  extends Model<GlobalSettingAttributes, GlobalSettingCreationAttributes>\n  implements GlobalSettingAttributes\n{\n  public id!: number;\n  public httpProxyHost!: string;\n  public httpProxyPort!: number;\n  public isProxyEnabled!: boolean;\n  public CommonUserCode!: number;\n  public AdminUserCode!: number;\n}\n\nGlobalSetting.init(\n  {\n    id: {\n      type: DataTypes.INTEGER,\n      autoIncrement: true,\n      primaryKey: true,\n    },\n    httpProxyHost: {\n      type: DataTypes.STRING,\n      allowNull: false,\n      defaultValue: \"127.0.0.1\",\n    },\n    httpProxyPort: {\n      type: DataTypes.INTEGER,\n      allowNull: false,\n      defaultValue: 7890,\n    },\n    isProxyEnabled: {\n      type: DataTypes.BOOLEAN,\n      allowNull: false,\n      defaultValue: true,\n    },\n    CommonUserCode: {\n      type: DataTypes.INTEGER,\n      allowNull: true,\n      defaultValue: 9527,\n    },\n    AdminUserCode: {\n      type: DataTypes.INTEGER,\n      allowNull: false,\n      defaultValue: 230713,\n    },\n  },\n  {\n    sequelize,\n    modelName: \"GlobalSetting\",\n    tableName: \"global_settings\",\n  }\n);\n\nexport default GlobalSetting;\n"
  },
  {
    "path": "backend/src/models/User.ts",
    "content": "import { DataTypes, Model, Optional } from \"sequelize\";\nimport sequelize from \"../config/database\";\n\ninterface UserAttributes {\n  id: number;\n  userId?: number;\n  username: string;\n  password: string;\n  role: number; // 修改为数字类型\n}\n\ninterface UserCreationAttributes extends Optional<UserAttributes, \"id\"> {}\n\nclass User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {\n  public id!: number;\n  public userId!: number;\n  public username!: string;\n  public password!: string;\n  public role!: number; // 实现数字类型的角色属性\n}\n\nUser.init(\n  {\n    id: {\n      type: DataTypes.INTEGER,\n      autoIncrement: true,\n      primaryKey: true,\n      allowNull: false, // 显式设置为不可为空\n    },\n    userId: {\n      type: DataTypes.UUID, // 对外暴露的不可预测ID\n      defaultValue: DataTypes.UUIDV4,\n      unique: true,\n      allowNull: false, // 显式设置为不可为空\n    },\n    username: {\n      type: DataTypes.STRING,\n      allowNull: false,\n      unique: true,\n    },\n    password: {\n      type: DataTypes.STRING,\n      allowNull: false,\n    },\n    role: {\n      type: DataTypes.INTEGER, // 修改为数字类型\n      allowNull: false,\n      defaultValue: 0, // 默认值为普通用户\n    },\n  },\n  {\n    sequelize,\n    modelName: \"User\",\n    tableName: \"users\",\n  }\n);\n\n// 角色映射\n// 0: 普通用户\n// 1: 管理员\n\nexport default User;\n"
  },
  {
    "path": "backend/src/models/UserSetting.ts",
    "content": "import { DataTypes, Model, Optional } from \"sequelize\";\nimport sequelize from \"../config/database\";\nimport User from \"./User\";\n\ninterface UserSettingAttributes {\n  id: number;\n  userId: string;\n  cloud115UserId?: string;\n  cloud115Cookie: string;\n  quarkCookie: string;\n}\n\ninterface UserSettingCreationAttributes extends Optional<UserSettingAttributes, \"id\"> {}\n\nclass UserSetting\n  extends Model<UserSettingAttributes, UserSettingCreationAttributes>\n  implements UserSettingAttributes\n{\n  public id!: number;\n  public userId!: string;\n  public cloud115UserId?: string;\n  public cloud115Cookie!: string;\n  public quarkCookie!: string;\n}\n\nUserSetting.init(\n  {\n    id: {\n      type: DataTypes.INTEGER,\n      autoIncrement: true,\n      primaryKey: true,\n    },\n    userId: {\n      type: DataTypes.UUID,\n      allowNull: false,\n      unique: true,\n      references: {\n        model: User,\n        key: \"userId\",\n      },\n      onDelete: \"CASCADE\",\n    },\n    cloud115UserId: {\n      type: DataTypes.STRING,\n      allowNull: true,\n    },\n    cloud115Cookie: {\n      type: DataTypes.STRING,\n      allowNull: true,\n    },\n    quarkCookie: {\n      type: DataTypes.STRING,\n      allowNull: true,\n    },\n  },\n  {\n    sequelize,\n    modelName: \"UserSetting\",\n    tableName: \"user_settings\",\n  }\n);\n\nUser.hasOne(UserSetting, {\n  foreignKey: \"userId\",\n  as: \"settings\",\n});\nUserSetting.belongsTo(User, {\n  foreignKey: \"userId\",\n  as: \"user\",\n});\n\nexport default UserSetting;\n"
  },
  {
    "path": "backend/src/routes/api.ts",
    "content": "import { Router } from \"express\";\nimport { container } from \"../inversify.config\";\nimport { TYPES } from \"../core/types\";\nimport { Cloud115Controller } from \"../controllers/cloud115\";\nimport { QuarkController } from \"../controllers/quark\";\nimport { ResourceController } from \"../controllers/resource\";\nimport { DoubanController } from \"../controllers/douban\";\nimport { ImageController } from \"../controllers/teleImages\";\nimport { SettingController } from \"../controllers/setting\";\nimport { UserController } from \"../controllers/user\";\nimport { SponsorsController } from \"../controllers/sponsors\";\n\nconst router = Router();\n\n// 获取控制器实例\nconst cloud115Controller = container.get<Cloud115Controller>(TYPES.Cloud115Controller);\nconst quarkController = container.get<QuarkController>(TYPES.QuarkController);\nconst resourceController = container.get<ResourceController>(TYPES.ResourceController);\nconst doubanController = container.get<DoubanController>(TYPES.DoubanController);\nconst imageController = container.get<ImageController>(TYPES.ImageController);\nconst settingController = container.get<SettingController>(TYPES.SettingController);\nconst userController = container.get<UserController>(TYPES.UserController);\nconst sponsorsController = container.get<SponsorsController>(TYPES.SponsorsController);\n\n// 用户相关路由\nrouter.post(\"/user/login\", (req, res) => userController.login(req, res));\nrouter.post(\"/user/register\", (req, res) => userController.register(req, res));\n\n// 图片相关路由\nrouter.get(\"/tele-images\", (req, res) => imageController.getImages(req, res));\n\n// 设置相关路由\nrouter.get(\"/setting/get\", (req, res) => settingController.get(req, res));\nrouter.post(\"/setting/save\", (req, res) => settingController.save(req, res));\n\n// 资源搜索\nrouter.get(\"/search\", (req, res) => resourceController.search(req, res));\n\n// 获取赞助者列表\nrouter.get(\"/sponsors\", (req, res) => sponsorsController.get(req, res));\n\n// 115网盘相关\nrouter.get(\"/cloud115/share-info\", (req, res) => cloud115Controller.getShareInfo(req, res));\nrouter.get(\"/cloud115/folders\", (req, res) => cloud115Controller.getFolderList(req, res));\nrouter.post(\"/cloud115/save\", (req, res) => cloud115Controller.saveFile(req, res));\n\n// 夸克网盘相关\nrouter.get(\"/quark/share-info\", (req, res) => quarkController.getShareInfo(req, res));\nrouter.get(\"/quark/folders\", (req, res) => quarkController.getFolderList(req, res));\nrouter.post(\"/quark/save\", (req, res) => quarkController.saveFile(req, res));\n\n// 获取豆瓣热门列表\nrouter.get(\"/douban/hot\", (req, res) => doubanController.getDoubanHotList(req, res));\n\nexport default router;\n"
  },
  {
    "path": "backend/src/services/Cloud115Service.ts",
    "content": "import { AxiosHeaders, AxiosInstance } from \"axios\"; // 导入 AxiosHeaders\nimport { createAxiosInstance } from \"../utils/axiosInstance\";\nimport { ShareInfoResponse, FolderListResponse, SaveFileParams } from \"../types/cloud\";\nimport { injectable } from \"inversify\";\nimport { Request } from \"express\";\nimport UserSetting from \"../models/UserSetting\";\nimport { ICloudStorageService } from \"@/types/services\";\nimport { logger } from \"../utils/logger\";\n\ninterface Cloud115ListItem {\n  cid: string;\n  n: string;\n  s: number;\n}\n\ninterface Cloud115FolderItem {\n  cid: string;\n  n: string;\n  ns: number;\n}\n\n@injectable()\nexport class Cloud115Service implements ICloudStorageService {\n  private api: AxiosInstance;\n  private cookie: string = \"\";\n\n  constructor() {\n    this.api = createAxiosInstance(\n      \"https://webapi.115.com\",\n      AxiosHeaders.from({\n        Host: \"webapi.115.com\",\n        Connection: \"keep-alive\",\n        xweb_xhr: \"1\",\n        Origin: \"\",\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"User-Agent\":\n          \"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\",\n        Accept: \"*/*\",\n        \"Sec-Fetch-Site\": \"cross-site\",\n        \"Sec-Fetch-Mode\": \"cors\",\n        \"Sec-Fetch-Dest\": \"empty\",\n        Referer: \"https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html\",\n        \"Accept-Encoding\": \"gzip, deflate, br\",\n        \"Accept-Language\": \"zh-CN,zh;q=0.9\",\n      })\n    );\n\n    this.api.interceptors.request.use((config) => {\n      config.headers.cookie = this.cookie;\n      return config;\n    });\n  }\n\n  async setCookie(req: Request): Promise<void> {\n    const userId = req.user?.userId;\n    const userSetting = await UserSetting.findOne({\n      where: { userId },\n    });\n    if (userSetting && userSetting.dataValues.cloud115Cookie) {\n      this.cookie = userSetting.dataValues.cloud115Cookie;\n    } else {\n      throw new Error(\"请先设置115网盘cookie\");\n    }\n  }\n\n  async getShareInfo(shareCode: string, receiveCode = \"\"): Promise<ShareInfoResponse> {\n    const response = await this.api.get(\"/share/snap\", {\n      params: {\n        share_code: shareCode,\n        receive_code: receiveCode,\n        offset: 0,\n        limit: 20,\n        cid: \"\",\n      },\n    });\n    if (response.data?.state && response.data.data?.list?.length > 0) {\n      return {\n        data: {\n          list: response.data.data.list.map((item: Cloud115ListItem) => ({\n            fileId: item.cid,\n            fileName: item.n,\n            fileSize: item.s,\n          })),\n        },\n      };\n    } else {\n      logger.error(\"未找到文件信息:\", response.data);\n      throw new Error(\"未找到文件信息\");\n    }\n  }\n\n  async getFolderList(parentCid = \"0\"): Promise<FolderListResponse> {\n    const response = await this.api.get(\"/files\", {\n      params: {\n        aid: 1,\n        cid: parentCid,\n        o: \"user_ptime\",\n        asc: 1,\n        offset: 0,\n        show_dir: 1,\n        limit: 50,\n        type: 0,\n        format: \"json\",\n        star: 0,\n        suffix: \"\",\n        natsort: 0,\n        snap: 0,\n        record_open_time: 1,\n        fc_mix: 0,\n      },\n    });\n    if (response.data?.state) {\n      return {\n        data: response.data.data\n          .filter((item: Cloud115FolderItem) => item.cid && !!item.ns)\n          .map((folder: Cloud115FolderItem) => ({\n            cid: folder.cid,\n            name: folder.n,\n            path: response.data.path,\n          })),\n      };\n    } else {\n      logger.error(\"获取目录列表失败:\", response.data.error);\n      throw new Error(\"获取115pan目录列表失败:\" + response.data.error);\n    }\n  }\n\n  async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {\n    const param = new URLSearchParams({\n      cid: params.folderId || \"\",\n      share_code: params.shareCode || \"\",\n      receive_code: params.receiveCode || \"\",\n      file_id: params.fids?.[0] || \"\",\n    });\n    const response = await this.api.post(\"/share/receive\", param.toString());\n    logger.info(\"保存文件:\", response.data);\n    if (response.data.state) {\n      return {\n        message: response.data.error,\n        data: response.data.data,\n      };\n    } else {\n      logger.error(\"保存文件失败:\", response.data.error);\n      throw new Error(\"保存115pan文件失败:\" + response.data.error);\n    }\n  }\n}\n"
  },
  {
    "path": "backend/src/services/DatabaseService.ts",
    "content": "import { Sequelize, QueryTypes } from \"sequelize\";\nimport GlobalSetting from \"../models/GlobalSetting\";\nimport { Searcher } from \"./Searcher\";\nimport sequelize from \"../config/database\";\n\n// 全局设置默认值\nconst DEFAULT_GLOBAL_SETTINGS = {\n  httpProxyHost: \"127.0.0.1\",\n  httpProxyPort: 7890,\n  isProxyEnabled: false,\n  CommonUserCode: 9527,\n  AdminUserCode: 230713,\n};\n\nexport class DatabaseService {\n  private sequelize: Sequelize;\n\n  constructor() {\n    this.sequelize = sequelize;\n  }\n\n  async initialize(): Promise<void> {\n    try {\n      await this.sequelize.query(\"PRAGMA foreign_keys = OFF\");\n      await this.cleanupBackupTables();\n      await this.sequelize.sync({ alter: true });\n      await this.sequelize.query(\"PRAGMA foreign_keys = ON\");\n      await this.initializeGlobalSettings();\n    } catch (error) {\n      throw new Error(`数据库初始化失败: ${(error as Error).message}`);\n    }\n  }\n\n  private async initializeGlobalSettings(): Promise<void> {\n    try {\n      const settings = await GlobalSetting.findOne();\n      if (!settings) {\n        await GlobalSetting.create(DEFAULT_GLOBAL_SETTINGS);\n        console.log(\"✅ Global settings initialized with default values.\");\n      }\n      await Searcher.updateAxiosInstance();\n    } catch (error) {\n      console.error(\"❌ Failed to initialize global settings:\", error);\n      throw error;\n    }\n  }\n\n  private async cleanupBackupTables(): Promise<void> {\n    const backupTables = await this.sequelize.query<{ name: string }>(\n      \"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%\\\\_backup%' ESCAPE '\\\\'\",\n      { type: QueryTypes.SELECT }\n    );\n\n    for (const table of backupTables) {\n      if (table?.name) {\n        await this.sequelize.query(`DROP TABLE IF EXISTS ${table.name}`);\n      }\n    }\n  }\n\n  // ... 其他数据库相关方法\n}\n"
  },
  {
    "path": "backend/src/services/DoubanService.ts",
    "content": "import { AxiosHeaders, AxiosInstance } from \"axios\";\nimport { createAxiosInstance } from \"../utils/axiosInstance\";\n\ninterface DoubanSubject {\n  id: string;\n  title: string;\n  rate: string;\n  cover: string;\n  url: string;\n  is_new: boolean;\n}\n\nexport class DoubanService {\n  private baseUrl: string;\n  private api: AxiosInstance;\n\n  constructor() {\n    this.baseUrl = \"https://movie.douban.com/j\";\n    this.api = createAxiosInstance(\n      this.baseUrl,\n      AxiosHeaders.from({\n        accept: \"*/*\",\n        \"accept-language\": \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\",\n        priority: \"u=1, i\",\n        \"sec-ch-ua\": '\"Not A(Brand\";v=\"8\", \"Chromium\";v=\"132\", \"Microsoft Edge\";v=\"132\"',\n        \"sec-ch-ua-mobile\": \"?0\",\n        \"sec-ch-ua-platform\": '\"Windows\"',\n        \"sec-fetch-dest\": \"empty\",\n        \"sec-fetch-mode\": \"cors\",\n        \"sec-fetch-site\": \"same-origin\",\n        \"x-requested-with\": \"XMLHttpRequest\",\n        cookie:\n          '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',\n        Referer: \"https://movie.douban.com/\",\n        \"Referrer-Policy\": \"unsafe-url\",\n      })\n    );\n  }\n\n  async getHotList(params: {\n    type: string;\n    tag: string;\n    page_limit: string;\n    page_start: string;\n  }): Promise<{ data: DoubanSubject[] }> {\n    try {\n      const response = await this.api.get(\"/search_subjects\", {\n        params: params,\n      });\n      if (response.data && response.data.subjects) {\n        return {\n          data: response.data.subjects,\n        };\n      } else {\n        return {\n          data: [],\n        };\n      }\n    } catch (error) {\n      console.error(\"Error fetching hot list:\", error);\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "backend/src/services/ImageService.ts",
    "content": "import { injectable } from \"inversify\";\nimport axios, { AxiosInstance } from \"axios\";\nimport tunnel from \"tunnel\";\nimport GlobalSetting from \"../models/GlobalSetting\";\nimport { GlobalSettingAttributes } from \"../models/GlobalSetting\";\n\n@injectable()\nexport class ImageService {\n  private axiosInstance: AxiosInstance | null = null;\n\n  constructor() {\n    // 移除构造函数中的初始化，改为懒加载\n  }\n\n  private async ensureAxiosInstance(): Promise<AxiosInstance> {\n    if (!this.axiosInstance) {\n      const settings = await GlobalSetting.findOne();\n      const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);\n\n      this.axiosInstance = axios.create({\n        timeout: 30000,\n        headers: {\n          Accept: \"image/*, */*\",\n          \"User-Agent\":\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\",\n        },\n        withCredentials: false,\n        maxRedirects: 5,\n        httpsAgent: globalSetting.isProxyEnabled\n          ? tunnel.httpsOverHttp({\n              proxy: {\n                host: globalSetting.httpProxyHost,\n                port: globalSetting.httpProxyPort,\n                headers: {\n                  \"Proxy-Authorization\": \"\",\n                },\n              },\n            })\n          : undefined,\n      });\n\n      this.axiosInstance.interceptors.response.use(\n        (response) => response,\n        (error) => {\n          throw error;\n        }\n      );\n    }\n    return this.axiosInstance;\n  }\n\n  async updateAxiosInstance(): Promise<void> {\n    this.axiosInstance = null;\n    await this.ensureAxiosInstance();\n  }\n\n  async getImages(url: string): Promise<any> {\n    const axiosInstance = await this.ensureAxiosInstance();\n\n    return await axiosInstance.get(url, {\n      responseType: \"stream\",\n      validateStatus: (status) => status >= 200 && status < 300,\n      headers: {\n        Referer: new URL(url).origin,\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "backend/src/services/QuarkService.ts",
    "content": "import { AxiosInstance, AxiosHeaders } from \"axios\";\nimport { logger } from \"../utils/logger\";\nimport { createAxiosInstance } from \"../utils/axiosInstance\";\nimport { injectable } from \"inversify\";\nimport { Request } from \"express\";\nimport UserSetting from \"../models/UserSetting\";\nimport {\n  ShareInfoResponse,\n  FolderListResponse,\n  QuarkFolderItem,\n  SaveFileParams,\n} from \"../types/cloud\";\nimport { ICloudStorageService } from \"@/types/services\";\n\ninterface QuarkShareInfo {\n  stoken?: string;\n  pwdId?: string;\n  fileSize?: number;\n  list: {\n    fid: string;\n    file_name: string;\n    file_type: number;\n    share_fid_token: string;\n  }[];\n}\n\n@injectable()\nexport class QuarkService implements ICloudStorageService {\n  private api: AxiosInstance;\n  private cookie: string = \"\";\n\n  constructor() {\n    this.api = createAxiosInstance(\n      \"https://drive-h.quark.cn\",\n      AxiosHeaders.from({\n        cookie: this.cookie,\n        accept: \"application/json, text/plain, */*\",\n        \"accept-language\": \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\",\n        \"content-type\": \"application/json\",\n        priority: \"u=1, i\",\n        \"sec-ch-ua\": '\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"',\n        \"sec-ch-ua-mobile\": \"?0\",\n        \"sec-ch-ua-platform\": '\"Windows\"',\n        \"sec-fetch-dest\": \"empty\",\n        \"sec-fetch-mode\": \"cors\",\n        \"sec-fetch-site\": \"same-site\",\n      })\n    );\n\n    this.api.interceptors.request.use((config) => {\n      config.headers.cookie = this.cookie;\n      return config;\n    });\n  }\n\n  async setCookie(req: Request): Promise<void> {\n    const userId = req.user?.userId;\n    const userSetting = await UserSetting.findOne({\n      where: { userId },\n    });\n    if (userSetting && userSetting.dataValues.quarkCookie) {\n      this.cookie = userSetting.dataValues.quarkCookie;\n    } else {\n      throw new Error(\"请先设置夸克网盘cookie\");\n    }\n  }\n\n  async getShareInfo(pwdId: string, passcode = \"\"): Promise<ShareInfoResponse> {\n    const response = await this.api.post(\n      `/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,\n      {\n        pwd_id: pwdId,\n        passcode,\n      }\n    );\n    if (response.data?.status === 200 && response.data.data) {\n      const fileInfo = response.data.data;\n      if (fileInfo.stoken) {\n        const res = await this.getShareList(pwdId, fileInfo.stoken);\n        return {\n          data: res,\n        };\n      }\n    }\n    throw new Error(\"获取夸克分享信息失败\");\n  }\n\n  async getShareList(pwdId: string, stoken: string): Promise<ShareInfoResponse[\"data\"]> {\n    const response = await this.api.get(\"/1/clouddrive/share/sharepage/detail\", {\n      params: {\n        pr: \"ucpro\",\n        fr: \"pc\",\n        uc_param_str: \"\",\n        pwd_id: pwdId,\n        stoken: stoken,\n        pdir_fid: \"0\",\n        force: \"0\",\n        _page: \"1\",\n        _size: \"50\",\n        _fetch_banner: \"1\",\n        _fetch_share: \"1\",\n        _fetch_total: \"1\",\n        _sort: \"file_type:asc,updated_at:desc\",\n        __dt: \"1589\",\n        __t: Date.now(),\n      },\n    });\n    if (response.data?.data) {\n      const list = response.data.data.list\n        .filter((item: QuarkShareInfo[\"list\"][0]) => item.fid)\n        .map((folder: QuarkShareInfo[\"list\"][0]) => ({\n          fileId: folder.fid,\n          fileName: folder.file_name,\n          fileIdToken: folder.share_fid_token,\n        }));\n      return {\n        list,\n        pwdId,\n        stoken,\n        fileSize: response.data.data.share?.size || 0,\n      };\n    } else {\n      return {\n        list: [],\n      };\n    }\n  }\n\n  async getFolderList(parentCid = \"0\"): Promise<FolderListResponse> {\n    const response = await this.api.get(\"/1/clouddrive/file/sort\", {\n      params: {\n        pr: \"ucpro\",\n        fr: \"pc\",\n        uc_param_str: \"\",\n        pdir_fid: parentCid,\n        _page: \"1\",\n        _size: \"100\",\n        _fetch_total: \"false\",\n        _fetch_sub_dirs: \"1\",\n        _sort: \"\",\n        __dt: \"2093126\",\n        __t: Date.now(),\n      },\n    });\n    if (response.data?.data && response.data.data.list) {\n      const data = response.data.data.list\n        .filter((item: QuarkFolderItem) => item.fid && item.file_type === 0)\n        .map((folder: QuarkFolderItem) => ({\n          cid: folder.fid,\n          name: folder.file_name,\n          path: [],\n        }));\n      return {\n        data,\n      };\n    } else {\n      const message = \"获取夸克目录列表失败:\" + response.data.error;\n      logger.error(message);\n      throw new Error(message);\n    }\n  }\n\n  async saveSharedFile(params: SaveFileParams): Promise<{ message: string; data: unknown }> {\n    const quarkParams = {\n      fid_list: params.fids,\n      fid_token_list: params.fidTokens,\n      to_pdir_fid: params.folderId,\n      pwd_id: params.shareCode,\n      stoken: params.receiveCode,\n      pdir_fid: \"0\",\n      scene: \"link\",\n    };\n    try {\n      const response = await this.api.post(\n        `/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,\n        quarkParams\n      );\n\n      return {\n        message: response.data.message,\n        data: response.data.data,\n      };\n    } catch (error) {\n      throw new Error(error instanceof Error ? error.message : \"未知错误\");\n    }\n  }\n}\n"
  },
  {
    "path": "backend/src/services/Searcher.ts",
    "content": "import { AxiosInstance, AxiosHeaders } from \"axios\";\nimport { createAxiosInstance } from \"../utils/axiosInstance\";\nimport GlobalSetting from \"../models/GlobalSetting\";\nimport { GlobalSettingAttributes } from \"../models/GlobalSetting\";\nimport * as cheerio from \"cheerio\";\nimport { config } from \"../config\";\nimport { logger } from \"../utils/logger\";\nimport { injectable } from \"inversify\";\n\ninterface sourceItem {\n  messageId?: string;\n  title?: string;\n  completeTitle?: string;\n  link?: string;\n  pubDate?: string;\n  content?: string;\n  description?: string;\n  image?: string;\n  cloudLinks?: string[];\n  tags?: string[];\n  cloudType?: string;\n}\n\n@injectable()\nexport class Searcher {\n  private static instance: Searcher;\n  private api: AxiosInstance | null = null;\n\n  constructor() {\n    this.initAxiosInstance();\n    Searcher.instance = this;\n  }\n\n  private async initAxiosInstance(isUpdate: boolean = false) {\n    let globalSetting = {} as GlobalSettingAttributes;\n    if (isUpdate) {\n      const settings = await GlobalSetting.findOne();\n      globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);\n    }\n    this.api = createAxiosInstance(\n      config.telegram.baseUrl,\n      AxiosHeaders.from({\n        accept:\n          \"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\",\n        \"accept-language\": \"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\",\n        \"cache-control\": \"max-age=0\",\n        priority: \"u=0, i\",\n        \"sec-ch-ua\": '\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"',\n        \"sec-ch-ua-mobile\": \"?0\",\n        \"sec-ch-ua-platform\": '\"macOS\"',\n        \"sec-fetch-dest\": \"document\",\n        \"sec-fetch-mode\": \"navigate\",\n        \"sec-fetch-site\": \"none\",\n        \"sec-fetch-user\": \"?1\",\n        \"upgrade-insecure-requests\": \"1\",\n      }),\n      globalSetting?.isProxyEnabled,\n      globalSetting?.isProxyEnabled\n        ? { host: globalSetting?.httpProxyHost, port: globalSetting?.httpProxyPort }\n        : undefined\n    );\n  }\n\n  public static async updateAxiosInstance(): Promise<void> {\n    await Searcher.instance.initAxiosInstance(true);\n  }\n\n  private extractCloudLinks(text: string): { links: string[]; cloudType: string } {\n    const links: string[] = [];\n    let cloudType = \"\";\n    Object.values(config.cloudPatterns).forEach((pattern, index) => {\n      const matches = text.match(pattern);\n      if (matches) {\n        links.push(...matches);\n        if (!cloudType) cloudType = Object.keys(config.cloudPatterns)[index];\n      }\n    });\n    return {\n      links: [...new Set(links)],\n      cloudType,\n    };\n  }\n\n  async searchAll(keyword: string, channelId?: string, messageId?: string) {\n    const allResults: any[] = [];\n\n    const channelList: any[] = channelId\n      ? config.telegram.channels.filter((channel: any) => channel.id === channelId)\n      : config.telegram.channels;\n\n    // 使用Promise.all进行并行请求\n    const searchPromises = channelList.map(async (channel) => {\n      try {\n        const messageIdparams = messageId ? `before=${messageId}` : \"\";\n        const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;\n        console.log(`Searching in channel ${channel.name} with URL: ${url}`);\n        return this.searchInWeb(url).then((results) => {\n          console.log(`Found ${results.items.length} items in channel ${channel.name}`);\n          if (results.items.length > 0) {\n            const channelResults = results.items\n              .filter((item: sourceItem) => item.cloudLinks && item.cloudLinks.length > 0)\n              .map((item: sourceItem) => ({\n                ...item,\n                channel: channel.name,\n                channelId: channel.id,\n              }));\n\n            allResults.push({\n              list: channelResults,\n              channelInfo: {\n                ...channel,\n                channelLogo: results.channelLogo,\n              },\n              id: channel.id,\n            });\n          }\n        });\n      } catch (error) {\n        logger.error(`搜索频道 ${channel.name} 失败:`, error);\n      }\n    });\n\n    // 等待所有请求完成\n    await Promise.all(searchPromises);\n\n    return {\n      data: allResults,\n    };\n  }\n\n  async searchInWeb(url: string) {\n    try {\n      if (!this.api) {\n        throw new Error(\"Axios instance is not initialized\");\n      }\n      const response = await this.api.get(url);\n      const html = response.data;\n      const $ = cheerio.load(html);\n      const items: sourceItem[] = [];\n      let channelLogo = \"\";\n      $(\".tgme_header_link\").each((_, element) => {\n        channelLogo = $(element).find(\"img\").attr(\"src\") || \"\";\n      });\n      // 遍历每个消息容器\n      $(\".tgme_widget_message_wrap\").each((_, element) => {\n        const messageEl = $(element);\n\n        // 通过 data-post 属性来获取消息的链接 去除channelId 获得消息id\n        const messageId = messageEl\n          .find(\".tgme_widget_message\")\n          .data(\"post\")\n          ?.toString()\n          .split(\"/\")[1];\n\n        // 提取标题 (第一个<br>标签前的内容)\n        const title =\n          messageEl\n            .find(\".js-message_text\")\n            .html()\n            ?.split(\"<br>\")[0]\n            .replace(/<[^>]+>/g, \"\")\n            .replace(/\\n/g, \"\") || \"\";\n\n        // 提取描述 (第一个<a>标签前面的内容，不包含标题)\n        const content =\n          messageEl\n            .find(\".js-message_text\")\n            .html()\n            ?.replace(title, \"\")\n            .split(\"<a\")[0]\n            .replace(/<br>/g, \"\")\n            .trim() || \"\";\n\n        // 提取链接 (消息中的链接)\n        // const link = messageEl.find('.tgme_widget_message').data('post');\n\n        // 提取发布时间\n        const pubDate = messageEl.find(\"time\").attr(\"datetime\");\n\n        // 提取图片\n        const image = messageEl\n          .find(\".tgme_widget_message_photo_wrap\")\n          .attr(\"style\")\n          ?.match(/url\\('(.+?)'\\)/)?.[1];\n\n        const tags: string[] = [];\n        // 提取云盘链接\n        const links = messageEl\n          .find(\".tgme_widget_message_text a\")\n          .map((_, el) => $(el).attr(\"href\"))\n          .get();\n        messageEl.find(\".tgme_widget_message_text a\").each((index, element) => {\n          const tagText = $(element).text();\n          if (tagText && tagText.startsWith(\"#\")) {\n            tags.push(tagText);\n          }\n        });\n        const cloudInfo = this.extractCloudLinks(links.join(\" \"));\n        // 添加到数组第一位\n        items.unshift({\n          messageId,\n          title,\n          pubDate,\n          content,\n          image,\n          cloudLinks: cloudInfo.links,\n          cloudType: cloudInfo.cloudType,\n          tags,\n        });\n      });\n      return { items: items, channelLogo };\n    } catch (error) {\n      logger.error(`搜索错误: ${url}`, error);\n      return {\n        items: [],\n        channelLogo: \"\",\n      };\n    }\n  }\n}\n\nexport default new Searcher();\n"
  },
  {
    "path": "backend/src/services/SettingService.ts",
    "content": "import { injectable, inject } from \"inversify\";\nimport { TYPES } from \"../core/types\";\nimport UserSetting from \"../models/UserSetting\";\nimport GlobalSetting from \"../models/GlobalSetting\";\nimport { Searcher } from \"./Searcher\";\nimport { ImageService } from \"./ImageService\";\n\n@injectable()\nexport class SettingService {\n  constructor(@inject(TYPES.ImageService) private imageService: ImageService) {}\n\n  async getSettings(userId: string | undefined, role: number | undefined) {\n    if (!userId) {\n      throw new Error(\"用户ID无效\");\n    }\n\n    let userSettings = await UserSetting.findOne({ where: { userId: userId.toString() } });\n    if (!userSettings) {\n      userSettings = await UserSetting.create({\n        userId: userId.toString(),\n        cloud115Cookie: \"\",\n        quarkCookie: \"\",\n      });\n    }\n\n    const globalSetting = await GlobalSetting.findOne();\n    return {\n      data: {\n        userSettings,\n        globalSetting: role === 1 ? globalSetting : null,\n      },\n    };\n  }\n\n  async saveSettings(userId: string | undefined, role: number | undefined, settings: any) {\n    if (!userId) {\n      throw new Error(\"用户ID无效\");\n    }\n\n    const { userSettings, globalSetting } = settings;\n    await UserSetting.update(userSettings, { where: { userId: userId.toString() } });\n\n    if (role === 1 && globalSetting) {\n      await GlobalSetting.update(globalSetting, { where: {} });\n    }\n    await this.updateSettings();\n    return { message: \"保存成功\" };\n  }\n\n  async updateSettings(/* 参数 */): Promise<void> {\n    // ... 其他代码 ...\n\n    // 修改这一行，使用注入的实例方法而不是静态方法\n    await this.imageService.updateAxiosInstance();\n    await Searcher.updateAxiosInstance();\n\n    // ... 其他代码 ...\n  }\n}\n"
  },
  {
    "path": "backend/src/services/SponsorsService.ts",
    "content": "import { injectable } from \"inversify\";\nimport { createAxiosInstance } from \"../utils/axiosInstance\";\nimport { AxiosInstance } from \"axios\";\nimport sponsors from \"../sponsors/sponsors.json\";\n\n@injectable()\nexport class SponsorsService {\n  private axiosInstance: AxiosInstance;\n\n  constructor() {\n    this.axiosInstance = createAxiosInstance(\"http://oss.jiangmuxin.cn/cloudsaver/\");\n  }\n  async getSponsors() {\n    try {\n      const response = await this.axiosInstance.get(\"sponsors.json\");\n      return {\n        data: response.data.sponsors,\n      };\n    } catch (error) {\n      return {\n        data: sponsors.sponsors,\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "backend/src/services/UserService.ts",
    "content": "import { injectable } from \"inversify\";\nimport bcrypt from \"bcrypt\";\nimport jwt from \"jsonwebtoken\";\nimport { config } from \"../config\";\nimport User from \"../models/User\";\nimport GlobalSetting from \"../models/GlobalSetting\";\n\n@injectable()\nexport class UserService {\n  private isValidInput(input: string): boolean {\n    // 检查是否包含空格或汉字\n    const regex = /^[^\\s\\u4e00-\\u9fa5]+$/;\n    return regex.test(input);\n  }\n\n  async register(username: string, password: string, registerCode: string) {\n    const globalSetting = await GlobalSetting.findOne();\n    const registerCodeList = [\n      globalSetting?.dataValues.CommonUserCode,\n      globalSetting?.dataValues.AdminUserCode,\n    ];\n    if (!registerCode || !registerCodeList.includes(Number(registerCode))) {\n      throw new Error(\"注册码错误\");\n    }\n\n    // 验证输入\n    if (!this.isValidInput(username) || !this.isValidInput(password)) {\n      throw new Error(\"用户名、密码或注册码不能包含空格或汉字\");\n    }\n\n    // 检查用户名是否已存在\n    const existingUser = await User.findOne({ where: { username } });\n    if (existingUser) {\n      throw new Error(\"用户名已存在\");\n    }\n\n    const hashedPassword = await bcrypt.hash(password, 10);\n    const role = registerCodeList.findIndex((x) => x === Number(registerCode));\n    const user = await User.create({ username, password: hashedPassword, role });\n\n    return {\n      data: user,\n      message: \"用户注册成功\",\n    };\n  }\n\n  async login(username: string, password: string) {\n    const user = await User.findOne({ where: { username } });\n    if (!user || !(await bcrypt.compare(password, user.password))) {\n      throw new Error(\"用户名或密码错误\");\n    }\n\n    const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {\n      expiresIn: \"6h\",\n    });\n\n    return {\n      data: {\n        token,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "backend/src/sponsors/sponsors.json",
    "content": "{\n  \"sponsors\": [\n    {\n      \"name\": \"立本狗头\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks1.jpg\",\n      \"message\": \"怒搓楼上狗头！ \"\n    },\n    {\n      \"name\": \"帝国鼻屎\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks2.jpg\",\n      \"message\": \"芜湖起飞！ \"\n    },\n    {\n      \"name\": \"雷霆222\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks3.jpg\",\n      \"message\": \"把我弄帅点 \"\n    },\n    {\n      \"name\": \"黑田奈奈子\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks4.jpg\",\n      \"message\": \"流年笑掷 未来可期 \",\n      \"link\": \"https://github.com/htnanako\"\n    },\n    {\n      \"name\": \"原野🐇\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks5.jpg\"\n    },\n    {\n      \"name\": \"我摆烂！\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks6.jpg\",\n      \"message\": \"人生苦短，及时行乐，卷什么卷，随缘摆烂 \"\n    },\n    {\n      \"name\": \"田培\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks7.jpg\"\n    },\n    {\n      \"name\": \"River\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks8.jpg\"\n    },\n    {\n      \"name\": \"午夜学徒\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks9.jpg\"\n    },\n    {\n      \"name\": \"阿潘\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks10.jpg\"\n    },\n    {\n      \"name\": \"闹闹黑\",\n      \"avatar\": \"http://oss.jiangmuxin.cn/cloudsaver/sponsors/thanks11.jpg\"\n    }\n  ]\n}\n"
  },
  {
    "path": "backend/src/types/cloud.ts",
    "content": "export interface ShareInfoResponse {\n  data: {\n    list: ShareInfoItem[];\n    fileSize?: number;\n    pwdId?: string;\n    stoken?: string;\n  };\n}\n\nexport interface GetShareInfoParams {\n  shareCode: string;\n  receiveCode?: string;\n}\n\nexport interface ShareInfoItem {\n  fileId: string;\n  fileName: string;\n  fileSize?: number;\n  fileIdToken?: string;\n}\nexport interface FolderListResponse {\n  data: {\n    cid: string;\n    name: string;\n    path: { cid: string; name: string }[];\n  }[];\n}\n\nexport interface SaveFileParams {\n  shareCode: string; // 分享code\n  receiveCode?: string; // 分享文件的密码\n  folderId?: string; // 文件夹id\n  fids?: string[]; // 存储文件id\n  fidTokens?: string[]; // 存储文件token\n}\n\nexport interface SaveFileResponse {\n  message: string;\n  data: unknown;\n}\n\nexport interface ShareFileInfo {\n  shareCode: string;\n  receiveCode?: string;\n  fileId: string;\n  cid?: string;\n  fid_list?: string[];\n  fid_token_list?: string[];\n  to_pdir_fid?: string;\n  pwd_id?: string;\n  stoken?: string;\n  pdir_fid?: string;\n  scene?: string;\n  [key: string]: any;\n}\n\nexport interface QuarkShareFileInfo {\n  fid_list: string[];\n  fid_token_list: string[];\n  to_pdir_fid: string;\n  pwd_id: string;\n  stoken: string;\n  pdir_fid: string;\n  scene: string;\n}\n\nexport interface QuarkShareInfo {\n  stoken?: string;\n  pwdId?: string;\n  fileSize?: number;\n  list: {\n    fid: string;\n    file_name: string;\n    file_type: number;\n    share_fid_token: string;\n  }[];\n}\n\nexport interface QuarkFolderItem {\n  fid: string;\n  file_name: string;\n  file_type: number;\n}\n"
  },
  {
    "path": "backend/src/types/cloud115.ts",
    "content": "export interface ShareInfo {\n  fileId: string;\n  fileName: string;\n  fileSize: number;\n}\n\nexport interface ShareInfoResponse {\n  data?: ShareInfo[];\n  message?: string;\n}\n"
  },
  {
    "path": "backend/src/types/express.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport { Request } from \"express\";\n\ndeclare module \"express\" {\n  interface Request {\n    user?: {\n      userId: string;\n      role: number;\n    };\n  }\n}\n"
  },
  {
    "path": "backend/src/types/index.ts",
    "content": "export interface Config {\n  app: {\n    port: number;\n    env: string;\n  };\n  database: {\n    type: string;\n    path: string;\n  };\n  jwt: {\n    secret: string;\n    expiresIn: string;\n  };\n  // ... 其他配置类型\n}\n"
  },
  {
    "path": "backend/src/types/services.ts",
    "content": "import { Request } from \"express\";\nimport { ShareInfoResponse, FolderListResponse, SaveFileParams } from \"./cloud\";\n\nexport interface ICloudStorageService {\n  setCookie(req: Request): Promise<void>;\n  getShareInfo(shareCode: string, receiveCode?: string): Promise<ShareInfoResponse>;\n  getFolderList(parentCid?: string): Promise<FolderListResponse>;\n  saveSharedFile(params: SaveFileParams): Promise<any>;\n}\n"
  },
  {
    "path": "backend/src/utils/axiosInstance.ts",
    "content": "import axios, { AxiosInstance, AxiosRequestHeaders } from \"axios\";\nimport tunnel from \"tunnel\";\n\ninterface ProxyConfig {\n  host: string;\n  port: number;\n}\n\nexport function createAxiosInstance(\n  baseURL: string,\n  headers?: AxiosRequestHeaders,\n  useProxy: boolean = false,\n  proxyConfig?: ProxyConfig\n): AxiosInstance {\n  let agent;\n  if (useProxy && proxyConfig) {\n    agent = tunnel.httpsOverHttp({\n      proxy: proxyConfig,\n    });\n  }\n\n  return axios.create({\n    baseURL,\n    timeout: 30000,\n    headers,\n    httpsAgent: useProxy ? agent : undefined,\n    withCredentials: true,\n  });\n}\n"
  },
  {
    "path": "backend/src/utils/handleError.ts",
    "content": "import { Response, NextFunction } from \"express\";\nimport { logger } from \"../utils/logger\";\n\ninterface CustomError {\n  name?: string;\n  message: string;\n  success?: boolean;\n}\n\nexport default function handleError(\n  res: Response,\n  error: CustomError | unknown,\n  message: string,\n  next: NextFunction\n) {\n  logger.error(message, error);\n  next(error || { success: false, message });\n}\n"
  },
  {
    "path": "backend/src/utils/index.ts",
    "content": "import jwt from \"jsonwebtoken\";\nimport { Request } from \"express\";\nimport { config } from \"../config\";\n\ninterface JwtPayload {\n  userId: string;\n}\n\nexport function getUserIdFromToken(req: Request): string | null {\n  try {\n    const token = req.headers.authorization?.split(\" \")[1];\n    if (!token) {\n      throw new Error(\"Token not found\");\n    }\n    const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;\n    return decoded.userId;\n  } catch (error) {\n    console.error(\"Invalid token:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "backend/src/utils/logger.ts",
    "content": "import winston from \"winston\";\nimport { config } from \"../config\";\n\nconst logger = winston.createLogger({\n  level: config.app.env === \"development\" ? \"debug\" : \"info\",\n  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),\n  transports: [\n    new winston.transports.File({ filename: \"logs/error.log\", level: \"error\" }),\n    new winston.transports.File({ filename: \"logs/combined.log\" }),\n  ],\n});\n\nif (config.app.env !== \"production\") {\n  logger.add(\n    new winston.transports.Console({\n      format: winston.format.simple(),\n    })\n  );\n}\n\nexport { logger };\n"
  },
  {
    "path": "backend/src/utils/response.ts",
    "content": "import { Response } from \"express\";\n\ninterface ResponseData {\n  code?: number; // 业务状态码\n  message?: string;\n  data?: any;\n}\n\nexport const sendSuccess = (res: Response, response: ResponseData, businessCode: number = 0) => {\n  response.code = businessCode;\n  res.status(200).json(response);\n};\n\nexport const sendError = (res: Response, response: ResponseData, businessCode: number = 10000) => {\n  response.code = businessCode;\n  res.status(200).json(response);\n};\n"
  },
  {
    "path": "backend/src/utils/responseHandler.ts",
    "content": "import { Response } from \"express\";\n\nexport const handleResponse = (res: Response, data: any, success: boolean) => {\n  res.json({ success, data });\n};\n"
  },
  {
    "path": "backend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"ES2020\"],\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"baseUrl\": \".\",\n    \"typeRoots\": [\"./node_modules/@types\", \"./src/types\"],\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "docker-entrypoint.sh",
    "content": "#!/bin/sh\n\n# 如果配置目录下没有 env 文件，则复制示例文件\nif [ ! -f /app/config/env ]; then\n    cp /app/.env.example /app/config/env\n    echo \"已创建默认配置文件 /app/config/env，请根据需要修改配置\"\nfi\n\n# 创建配置文件软链接\nln -sf /app/config/env /app/.env\n\n# 启动 Nginx 和后端服务\nnginx -g 'daemon off;' & npm run start "
  },
  {
    "path": "frontend/.env",
    "content": "VITE_API_BASE_URL=\"\"\nVITE_API_BASE_URL_PROXY=\"http://127.0.0.1:8009\""
  },
  {
    "path": "frontend/auto-imports.d.ts",
    "content": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin-auto-import\nexport {}\ndeclare global {\n  const ElMessage: typeof import('element-plus/es')['ElMessage']\n  const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']\n}\n"
  },
  {
    "path": "frontend/components.d.ts",
    "content": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// Generated by unplugin-vue-components\n// Read more: https://github.com/vuejs/core/pull/3399\nexport {}\n\ndeclare module 'vue' {\n  export interface GlobalComponents {\n    AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']\n    ElAside: typeof import('element-plus/es')['ElAside']\n    ElBacktop: typeof import('element-plus/es')['ElBacktop']\n    ElButton: typeof import('element-plus/es')['ElButton']\n    ElCard: typeof import('element-plus/es')['ElCard']\n    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']\n    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']\n    ElContainer: typeof import('element-plus/es')['ElContainer']\n    ElDialog: typeof import('element-plus/es')['ElDialog']\n    ElEmpty: typeof import('element-plus/es')['ElEmpty']\n    ElForm: typeof import('element-plus/es')['ElForm']\n    ElFormItem: typeof import('element-plus/es')['ElFormItem']\n    ElHeader: typeof import('element-plus/es')['ElHeader']\n    ElIcon: typeof import('element-plus/es')['ElIcon']\n    ElImage: typeof import('element-plus/es')['ElImage']\n    ElInput: typeof import('element-plus/es')['ElInput']\n    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']\n    ElLink: typeof import('element-plus/es')['ElLink']\n    ElMain: typeof import('element-plus/es')['ElMain']\n    ElMenu: typeof import('element-plus/es')['ElMenu']\n    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']\n    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']\n    ElSwitch: typeof import('element-plus/es')['ElSwitch']\n    ElTable: typeof import('element-plus/es')['ElTable']\n    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']\n    ElTabPane: typeof import('element-plus/es')['ElTabPane']\n    ElTabs: typeof import('element-plus/es')['ElTabs']\n    ElTag: typeof import('element-plus/es')['ElTag']\n    ElTooltip: typeof import('element-plus/es')['ElTooltip']\n    FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']\n    ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']\n    ResourceSelect: typeof import('./src/components/Home/ResourceSelect.vue')['default']\n    ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']\n    RouterLink: typeof import('vue-router')['RouterLink']\n    RouterView: typeof import('vue-router')['RouterView']\n    SearchBar: typeof import('./src/components/SearchBar.vue')['default']\n    VanBackTop: typeof import('vant/es')['BackTop']\n    VanButton: typeof import('vant/es')['Button']\n    VanCell: typeof import('vant/es')['Cell']\n    VanCellGroup: typeof import('vant/es')['CellGroup']\n    VanCheckbox: typeof import('vant/es')['Checkbox']\n    VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']\n    VanEmpty: typeof import('vant/es')['Empty']\n    VanField: typeof import('vant/es')['Field']\n    VanForm: typeof import('vant/es')['Form']\n    VanIcon: typeof import('vant/es')['Icon']\n    VanImage: typeof import('vant/es')['Image']\n    VanLoading: typeof import('vant/es')['Loading']\n    VanOverlay: typeof import('vant/es')['Overlay']\n    VanPopup: typeof import('vant/es')['Popup']\n    VanSearch: typeof import('vant/es')['Search']\n    VanSwitch: typeof import('vant/es')['Switch']\n    VanTab: typeof import('vant/es')['Tab']\n    VanTabbar: typeof import('vant/es')['Tabbar']\n    VanTabbarItem: typeof import('vant/es')['TabbarItem']\n    VanTabs: typeof import('vant/es')['Tabs']\n    VanTag: typeof import('vant/es')['Tag']\n  }\n  export interface ComponentCustomProperties {\n    vLoading: typeof import('element-plus/es')['ElLoadingDirective']\n  }\n}\n"
  },
  {
    "path": "frontend/index.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <!-- 移动端适配  -->\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no\"\n    />\n    <meta name=\"keywords\" content=\"网盘,资源搜索,云存储\" />\n    <!-- SEO关键词 -->\n    <meta name=\"description\" content=\"网盘资源搜索工具\" />\n    <!-- 设置Web App描述 -->\n    <meta name=\"theme-color\" content=\"#ffffff\" />\n    <!-- 设置主题颜色 -->\n    <meta property=\"og:title\" content=\"CloudSaver\" />\n    <!-- 社交媒体分享标题 -->\n    <meta property=\"og:description\" content=\"网盘资源搜索工具\" />\n    <!-- 社交媒体分享描述 -->\n    <meta property=\"og:url\" content=\"https://github.com/jiangrui1994/CloudSaver\" />\n    <!-- 社交媒体分享链接 -->\n    <meta name=\"twitter:card\" content=\"summary\" />\n    <!-- Twitter卡片类型 -->\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n    <!-- 开启Web App功能 -->\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\" />\n    <!-- 设置状态栏样式 -->\n    <meta name=\"apple-mobile-web-app-title\" content=\"CloudSaver\" />\n    <!-- 设置Web App标题 -->\n    <link rel=\"apple-touch-icon\" href=\"/logo-1.png\" />\n    <!-- 设置Web App图标 -->\n    <link rel=\"mask-icon\" href=\"/logo.svg\" color=\"transparent\" />\n    <!-- 设置Web App图标遮罩 -->\n    <meta name=\"referrer\" content=\"no-referrer\" />\n    <title>CloudSaver</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"cloud-saver-web\",\n  \"private\": true,\n  \"version\": \"0.2.5\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite --host\",\n    \"build\": \"vue-tsc && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@element-plus/icons-vue\": \"^2.3.1\",\n    \"axios\": \"^1.6.7\",\n    \"element-plus\": \"^2.6.1\",\n    \"gsap\": \"^3.12.7\",\n    \"pinia\": \"^2.1.7\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"typeit\": \"^8.8.7\",\n    \"vant\": \"^4.9.17\",\n    \"vue\": \"^3.4.21\",\n    \"vue-router\": \"^4.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.11.25\",\n    \"@vant/auto-import-resolver\": \"^1.3.0\",\n    \"@vitejs/plugin-vue\": \"^5.0.4\",\n    \"postcss-pxtorem\": \"^6.1.0\",\n    \"sass\": \"^1.83.4\",\n    \"typescript\": \"^5.4.2\",\n    \"unplugin-auto-import\": \"^0.17.8\",\n    \"unplugin-vue-components\": \"^0.26.0\",\n    \"vite\": \"^5.1.5\",\n    \"vite-plugin-pwa\": \"^0.21.1\",\n    \"vue-tsc\": \"^2.0.6\"\n  }\n}\n"
  },
  {
    "path": "frontend/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    \"postcss-pxtorem\": {\n      rootValue({ file }) {\n        return file.indexOf(\"vant\") !== -1 || file.indexOf(\"mobile\") !== -1 ? 50 : 75;\n      },\n      propList: [\"*\"],\n      exclude: (file) => {\n        return !file.includes(\"mobile\") && !file.includes(\"vant\");\n      },\n      minPixelValue: 2,\n      mediaQuery: false,\n    },\n  },\n};\n"
  },
  {
    "path": "frontend/src/App.vue",
    "content": "<template>\n  <el-config-provider>\n    <router-view />\n  </el-config-provider>\n</template>\n\n<style>\n#app {\n  height: 100vh;\n  width: 100%;\n  height: 100%;\n  overflow-y: auto;\n  -webkit-overflow-scrolling: touch;\n}\n:root {\n  --theme-color: #3e3e3e;\n  --theme-theme: #133ab3;\n  --theme-background: #fafafa;\n  --theme-other_background: #ffffff;\n}\nhtml,\nbody {\n  margin: 0;\n  font-size: 15px;\n  font-family:\n    v-sans,\n    system-ui,\n    -apple-system,\n    BlinkMacSystemFont,\n    Segoe UI,\n    sans-serif,\n    \"Apple Color Emoji\",\n    \"Segoe UI Emoji\",\n    Segoe UI Symbol;\n  line-height: 1.6;\n  color: var(--theme-color);\n  background-color: var(--theme-background);\n  word-wrap: break-word;\n}\n\nbody {\n  position: fixed;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n/* 移动端全局样式 */\n@media screen and (max-width: 768px) {\n  #app {\n    max-width: 100vw;\n    overflow-x: hidden;\n  }\n\n  /* 统一按钮样式 */\n  .van-button {\n    height: 40px;\n    font-size: var(--font-size-base);\n    border-radius: var(--border-radius-base);\n  }\n\n  /* 统一输入框样式 */\n  .van-field {\n    font-size: var(--font-size-base);\n  }\n\n  /* 统一卡片样式 */\n  .van-card {\n    border-radius: var(--border-radius-base);\n    margin: var(--spacing-base) 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/api/cloud115.ts",
    "content": "import request from \"@/utils/request\";\nimport type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from \"@/types\";\n\nexport const cloud115Api = {\n  async getShareInfo(params: GetShareInfoParams) {\n    const { data } = await request.get<ShareInfoResponse>(\"/api/cloud115/share-info\", {\n      params,\n    });\n    return data as ShareInfoResponse;\n  },\n\n  async getFolderList(parentCid = \"0\") {\n    const res = await request.get<Folder[]>(\"/api/cloud115/folders\", {\n      params: { parentCid },\n    });\n    return res;\n  },\n\n  async saveFile(params: SaveFileParams) {\n    const res = await request.post(\"/api/cloud115/save\", params);\n    return res;\n  },\n};\n"
  },
  {
    "path": "frontend/src/api/douban.ts",
    "content": "import request from \"@/utils/request\";\nimport { HotListItem, HotListParams } from \"@/types/douban\";\n\nexport const doubanApi = {\n  async getHotList(params: HotListParams) {\n    const { data } = await request.get<HotListItem[]>(\"/api/douban/hot\", {\n      params,\n    });\n    return data;\n  },\n};\n"
  },
  {
    "path": "frontend/src/api/quark.ts",
    "content": "import request from \"@/utils/request\";\nimport type { ShareInfoResponse, Folder, SaveFileParams, GetShareInfoParams } from \"@/types\";\n\nexport const quarkApi = {\n  async getShareInfo(params: GetShareInfoParams) {\n    const { data } = await request.get<ShareInfoResponse>(\"/api/quark/share-info\", {\n      params,\n    });\n    return data as ShareInfoResponse;\n  },\n\n  async getFolderList(parentCid = \"0\") {\n    const data = await request.get<Folder[]>(\"/api/quark/folders\", {\n      params: { parentCid },\n    });\n    return data;\n  },\n\n  async saveFile(params: SaveFileParams) {\n    return await request.post(\"/api/quark/save\", params);\n  },\n};\n"
  },
  {
    "path": "frontend/src/api/resource.ts",
    "content": "import request from \"@/utils/request\";\nimport type { Resource } from \"@/types/index\";\n\nexport const resourceApi = {\n  search(keyword: string, channelId?: string, lastMessageId?: string) {\n    return request.get<Resource[]>(`/api/search`, {\n      params: { keyword, channelId, lastMessageId },\n    });\n  },\n};\n"
  },
  {
    "path": "frontend/src/api/setting.ts",
    "content": "import request from \"@/utils/request\";\nimport type { GlobalSettingAttributes, UserSettingAttributes } from \"@/types\";\n\nexport const settingApi = {\n  getSetting: () => {\n    return request.get<{\n      userSettings: UserSettingAttributes;\n      globalSetting: GlobalSettingAttributes;\n    }>(\"/api/setting/get\");\n  },\n  saveSetting: (data: {\n    userSettings: UserSettingAttributes;\n    globalSetting?: GlobalSettingAttributes | null;\n  }) => {\n    return request.post(\"/api/setting/save\", data);\n  },\n};\n"
  },
  {
    "path": "frontend/src/api/user.ts",
    "content": "import request from \"@/utils/request\";\n\nexport const userApi = {\n  login: (data: { username: string; password: string }) => {\n    return request.post<{ token: string }>(\"/api/user/login\", data);\n  },\n  register: (data: { username: string; password: string; registerCode: string }) => {\n    return request.post<{ token: string }>(\"/api/user/register\", data);\n  },\n  getSponsors: () => {\n    return request.get(\"/api/sponsors?timestamp=\" + Date.now());\n  },\n};\n"
  },
  {
    "path": "frontend/src/components/AsideMenu.vue",
    "content": "<template>\n  <div class=\"pc-aside\">\n    <!-- Logo 区域 -->\n    <div class=\"pc-aside__logo\">\n      <img :src=\"logo\" alt=\"Cloud Saver Logo\" class=\"logo__image\" />\n      <h1 class=\"logo__title\">Cloud Saver</h1>\n    </div>\n\n    <!-- 菜单区域 -->\n    <el-menu\n      :default-active=\"currentMenu?.index || '1'\"\n      :default-openeds=\"currentMenuOpen\"\n      class=\"pc-aside__menu\"\n    >\n      <template v-for=\"menu in menuList\" :key=\"menu.index\">\n        <!-- 子菜单 -->\n        <el-sub-menu v-if=\"menu.children\" :index=\"menu.index\">\n          <template #title>\n            <el-icon><component :is=\"menu.icon\" /></el-icon>\n            <span>{{ menu.title }}</span>\n          </template>\n\n          <el-menu-item\n            v-for=\"child in menu.children\"\n            :key=\"child.index\"\n            :index=\"child.index\"\n            @click=\"handleMenuClick(child)\"\n          >\n            <span>{{ child.title }}</span>\n          </el-menu-item>\n        </el-sub-menu>\n\n        <!-- 普通菜单项 -->\n        <el-menu-item\n          v-else\n          :index=\"menu.index\"\n          :disabled=\"menu.disabled\"\n          @click=\"handleMenuClick(menu)\"\n        >\n          <el-icon><component :is=\"menu.icon\" /></el-icon>\n          <span>{{ menu.title }}</span>\n        </el-menu-item>\n      </template>\n    </el-menu>\n\n    <!-- GitHub 链接 -->\n    <div class=\"pc-aside__footer\">\n      <a :href=\"PROJECT_GITHUB\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"github-link\">\n        <svg\n          height=\"20\"\n          aria-hidden=\"true\"\n          viewBox=\"0 0 24 24\"\n          version=\"1.1\"\n          width=\"20\"\n          class=\"github-icon\"\n        >\n          <path\n            fill=\"currentColor\"\n            d=\"M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z\"\n          />\n        </svg>\n        <span>GitHub</span>\n        <span class=\"version\">v{{ pkg.version }}</span>\n      </a>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport { useRouter, useRoute } from \"vue-router\";\nimport { Search, Film, Setting, Link } from \"@element-plus/icons-vue\";\nimport logo from \"@/assets/images/logo.png\";\nimport { PROJECT_GITHUB } from \"@/constants/project\";\nimport pkg from \"../../package.json\";\n\n// 类型定义\ninterface MenuItem {\n  index: string;\n  title: string;\n  icon?: typeof Search | typeof Film | typeof Setting | typeof Link;\n  router?: string;\n  children?: MenuItem[];\n  disabled?: boolean;\n}\n\n// 路由相关\nconst router = useRouter();\nconst route = useRoute();\n\n// 菜单配置\nconst menuList: MenuItem[] = [\n  {\n    index: \"1\",\n    title: \"资源搜索\",\n    icon: Search,\n    router: \"/resource\",\n  },\n  {\n    index: \"2\",\n    title: \"豆瓣榜单\",\n    icon: Film,\n    children: [\n      {\n        index: \"2-1\",\n        title: \"热门电影\",\n        router: \"/douban?type=movie\",\n      },\n      {\n        index: \"2-2\",\n        title: \"热门电视剧\",\n        router: \"/douban?type=tv\",\n      },\n      {\n        index: \"2-3\",\n        title: \"最新电影\",\n        router: \"/douban?type=movie&tag=最新\",\n      },\n      {\n        index: \"2-4\",\n        title: \"热门综艺\",\n        router: \"/douban?type=tv&tag=综艺\",\n      },\n    ],\n  },\n  {\n    index: \"3\",\n    title: \"设置\",\n    icon: Setting,\n    router: \"/setting\",\n    disabled: false,\n  },\n  {\n    index: \"4\",\n    title: \"鸣谢\",\n    icon: Link,\n    router: \"/thanks\",\n  },\n];\n\n// 计算当前激活的菜单\nconst currentMenu = computed(() => {\n  const flatMenus = menuList.reduce<MenuItem[]>((acc, menu) => {\n    if (!menu.children) {\n      acc.push(menu);\n    } else {\n      acc.push(...menu.children);\n    }\n    return acc;\n  }, []);\n\n  return flatMenus.find((menu) => menu.router === decodeURIComponent(route.fullPath));\n});\n\n// 计算当前展开的子菜单\nconst currentMenuOpen = computed(() => {\n  if (currentMenu.value?.index.includes(\"-\")) {\n    return [currentMenu.value.index.split(\"-\")[0]];\n  }\n  return [];\n});\n\n// 菜单点击处理\nconst handleMenuClick = (menu: MenuItem) => {\n  if (menu.router) {\n    router.push(menu.router);\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.pc-aside {\n  height: 100%;\n  background: var(--theme-card-bg);\n  border-right: 1px solid rgba(0, 0, 0, 0.1);\n\n  // Logo 区域\n  &__logo {\n    @include flex-center;\n    padding: 24px 16px;\n    gap: 12px;\n\n    .logo__image {\n      width: 32px;\n      height: 32px;\n      object-fit: contain;\n    }\n\n    .logo__title {\n      margin: 0;\n      font-size: 18px;\n      font-weight: 600;\n      color: var(--theme-text-primary);\n      @include text-overflow;\n    }\n  }\n\n  // 菜单区域\n  &__menu {\n    border-right: none;\n    background: transparent;\n\n    :deep(.el-menu-item) {\n      height: 48px;\n      line-height: 48px;\n      color: var(--theme-text-regular);\n\n      &.is-active {\n        color: var(--theme-primary);\n        background: rgba(0, 102, 204, 0.1);\n      }\n\n      &:hover {\n        color: var(--theme-primary);\n        background: rgba(0, 102, 204, 0.05);\n      }\n    }\n\n    :deep(.el-sub-menu) {\n      .el-sub-menu__title {\n        color: var(--theme-text-regular);\n\n        &:hover {\n          color: var(--theme-primary);\n          background: rgba(0, 102, 204, 0.05);\n        }\n      }\n    }\n\n    :deep(.el-icon) {\n      font-size: 18px;\n      margin-right: 12px;\n      color: inherit;\n    }\n  }\n\n  // GitHub 链接区域\n  &__footer {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    padding: 16px;\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n    background: var(--theme-card-bg);\n\n    .github-link {\n      @include flex-center;\n      gap: 8px;\n      padding: 12px;\n      color: var(--theme-text-regular);\n      text-decoration: none;\n      border-radius: var(--theme-radius);\n      transition: var(--theme-transition);\n\n      .github-icon {\n        font-size: 20px;\n        transition: var(--theme-transition);\n      }\n\n      .version {\n        font-size: 12px;\n        opacity: 0.7;\n        margin-left: 4px;\n      }\n\n      &:hover {\n        color: var(--theme-primary);\n        background: rgba(0, 102, 204, 0.05);\n        transform: translateY(-1px);\n\n        .github-icon {\n          transform: scale(1.1);\n        }\n\n        .version {\n          opacity: 1;\n        }\n      }\n    }\n  }\n}\n\n// 自定义滚动条\n.pc-aside__menu {\n  height: calc(100vh - 80px - 69px); // 减去 logo 高度和 footer 高度\n  overflow-y: auto;\n\n  &::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: rgba(0, 0, 0, 0.2);\n    border-radius: 3px;\n\n    &:hover {\n      background: rgba(0, 0, 0, 0.3);\n    }\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Home/FolderSelect.vue",
    "content": "<template>\n  <div class=\"folder-select\">\n    <div class=\"folder-header\">\n      <div class=\"folder-path\">\n        <el-icon><FolderOpened /></el-icon>\n        <template v-if=\"folderPath.length\">\n          <span\n            v-for=\"(folder, index) in folderPath\"\n            :key=\"folder.cid\"\n            class=\"path-item\"\n            @click=\"handlePathClick(index)\"\n          >\n            <span class=\"folder-name\">{{ folder.name }}</span>\n            <el-icon v-if=\"index < folderPath.length - 1\"><ArrowRight /></el-icon>\n          </span>\n        </template>\n        <span v-else class=\"root-path\" @click=\"handlePathClick(-1)\">根目录</span>\n      </div>\n    </div>\n\n    <div class=\"folder-list\">\n      <div v-if=\"!folders.length\" class=\"empty-folder\">\n        <el-empty description=\"暂无文件夹\" />\n      </div>\n      <div\n        v-for=\"folder in folders\"\n        :key=\"folder.cid\"\n        class=\"folder-item\"\n        :class=\"{ 'is-selected': folder.cid === selectedFolder?.cid }\"\n        @click=\"handleFolderClick(folder)\"\n      >\n        <div class=\"folder-info\">\n          <el-icon><Folder /></el-icon>\n          <span class=\"folder-name\">{{ folder.name }}</span>\n        </div>\n        <el-icon class=\"arrow-icon\"><ArrowRight /></el-icon>\n      </div>\n    </div>\n\n    <div v-if=\"loading\" class=\"loading-overlay\">\n      <el-icon class=\"loading-icon\"><Loading /></el-icon>\n      <span>加载中...</span>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, defineProps } from \"vue\";\nimport { cloud115Api } from \"@/api/cloud115\";\nimport { quarkApi } from \"@/api/quark\";\nimport type { Folder as FolderType } from \"@/types\";\nimport { Folder, FolderOpened, ArrowRight, Loading } from \"@element-plus/icons-vue\";\n\nimport { ElMessage } from \"element-plus\";\n\nconst props = defineProps({\n  cloudType: {\n    type: String,\n    required: true,\n  },\n});\n\nconst loading = ref(false);\nconst folders = ref<FolderType[]>([]);\nconst selectedFolder = ref<FolderType | null>(null);\nconst folderPath = ref<FolderType[]>([{ name: \"根目录\", cid: \"0\" }]);\nconst emit = defineEmits<{\n  (e: \"select\", folderId: string): void;\n  (e: \"close\"): void;\n}>();\n\nconst cloudTypeApiMap = {\n  pan115: cloud115Api,\n  quark: quarkApi,\n};\n\nconst getList = async (cid: string = \"0\") => {\n  const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];\n  loading.value = true;\n  try {\n    const res = await api.getFolderList?.(cid);\n    if (res?.code === 0) {\n      folders.value = res.data || [];\n    } else {\n      throw new Error(res?.message);\n    }\n  } catch (error) {\n    ElMessage.error(error instanceof Error ? error.message : \"获取目录失败\");\n    emit(\"close\");\n  } finally {\n    loading.value = false;\n  }\n};\n\nconst handleFolderClick = async (folder: FolderType) => {\n  selectedFolder.value = folder;\n  folderPath.value = [...folderPath.value, folder];\n  emit(\"select\", folder.cid);\n  await getList(folder.cid);\n};\n\nconst handlePathClick = async (index: number) => {\n  if (index < 0) {\n    // 点击根目录\n    folderPath.value = [{ name: \"根目录\", cid: \"0\" }];\n    selectedFolder.value = null;\n    await getList(\"0\");\n  } else {\n    // 点击路径中的某个文件夹\n    const targetFolder = folderPath.value[index];\n    folderPath.value = folderPath.value.slice(0, index + 1);\n    selectedFolder.value = targetFolder;\n    await getList(targetFolder.cid);\n    emit(\"select\", targetFolder.cid);\n  }\n};\n\n// 初始化加载\ngetList();\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.folder-select {\n  position: relative;\n  min-height: 300px;\n  max-height: 500px;\n  display: flex;\n  flex-direction: column;\n  padding: 4px;\n\n  .folder-header {\n    position: sticky;\n    top: 0;\n    z-index: 1;\n    margin-bottom: 16px;\n    padding: 12px 16px;\n    background: var(--el-fill-color-light);\n    border-radius: var(--theme-radius);\n\n    .folder-path {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      color: var(--theme-text-regular);\n      font-size: 14px;\n      overflow-x: auto;\n\n      &::-webkit-scrollbar {\n        height: 4px;\n      }\n\n      &::-webkit-scrollbar-thumb {\n        background: rgba(0, 0, 0, 0.1);\n        border-radius: 2px;\n      }\n\n      .el-icon {\n        flex-shrink: 0;\n        font-size: 16px;\n        color: var(--theme-primary);\n      }\n\n      .path-item {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n        white-space: nowrap;\n        cursor: pointer;\n        transition: var(--theme-transition);\n\n        &:hover {\n          color: var(--theme-primary);\n\n          .folder-name {\n            color: var(--theme-primary);\n          }\n        }\n\n        .folder-name {\n          color: var(--theme-text-primary);\n        }\n      }\n\n      .root-path {\n        color: var(--theme-text-secondary);\n        cursor: pointer;\n        transition: var(--theme-transition);\n\n        &:hover {\n          color: var(--theme-primary);\n        }\n      }\n    }\n  }\n}\n\n.folder-list {\n  flex: 1;\n  overflow-y: auto;\n  padding: 4px;\n\n  .folder-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    border-radius: var(--theme-radius);\n    cursor: pointer;\n    transition: var(--theme-transition);\n\n    &:hover {\n      background: var(--el-fill-color-light);\n    }\n\n    &.is-selected {\n      background: var(--el-color-primary-light-9);\n      color: var(--theme-primary);\n\n      .el-icon {\n        color: var(--theme-primary);\n      }\n    }\n\n    .folder-info {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      font-size: 14px;\n\n      .el-icon {\n        font-size: 16px;\n        color: var(--theme-text-regular);\n      }\n\n      .folder-name {\n        color: var(--theme-text-primary);\n      }\n    }\n\n    .arrow-icon {\n      font-size: 16px;\n      color: var(--theme-text-secondary);\n    }\n  }\n}\n\n.empty-folder {\n  padding: 32px 0;\n}\n\n.loading-overlay {\n  @include flex-center;\n  position: absolute;\n  inset: 0;\n  background: rgba(255, 255, 255, 0.9);\n  backdrop-filter: blur(4px);\n  gap: 8px;\n  font-size: 14px;\n  color: var(--theme-text-regular);\n\n  .loading-icon {\n    font-size: 20px;\n    animation: rotating 2s linear infinite;\n  }\n}\n\n@keyframes rotating {\n  from {\n    transform: rotate(0);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Home/ResourceCard.vue",
    "content": "<template>\n  <div class=\"resource-card\">\n    <!-- 详情弹窗 -->\n    <el-dialog\n      v-model=\"showDetail\"\n      :title=\"currentResource?.title\"\n      width=\"700px\"\n      class=\"resource-detail-dialog\"\n      destroy-on-close\n    >\n      <div v-if=\"currentResource\" class=\"detail-content\">\n        <div class=\"detail-cover\">\n          <el-image\n            class=\"cover-image\"\n            :src=\"getProxyImageUrl(currentResource.image as string)\"\n            :fit=\"currentResource.image ? 'cover' : 'contain'\"\n          />\n          <el-tag\n            class=\"cloud-type\"\n            :type=\"store.tagColor[currentResource.cloudType as keyof TagColor]\"\n            effect=\"dark\"\n            round\n          >\n            {{ currentResource.cloudType }}\n          </el-tag>\n        </div>\n        <div class=\"detail-info\">\n          <h3 class=\"detail-title\">\n            <el-link :href=\"currentResource.cloudLinks[0]\" target=\"_blank\" :underline=\"false\">\n              {{ currentResource.title }}\n            </el-link>\n          </h3>\n          <div class=\"detail-description\" v-html=\"currentResource.content\" />\n          <div v-if=\"currentResource.tags?.length\" class=\"detail-tags\">\n            <div class=\"tags-list\">\n              <el-tag\n                v-for=\"tag in currentResource.tags\"\n                :key=\"tag\"\n                class=\"tag-item\"\n                @click=\"searchMovieforTag(tag)\"\n              >\n                {{ tag }}\n              </el-tag>\n            </div>\n          </div>\n        </div>\n      </div>\n      <template #footer>\n        <div class=\"dialog-footer\">\n          <el-button type=\"primary\" plain @click=\"currentResource && handleJump(currentResource)\"\n            >跳转</el-button\n          >\n          <el-button\n            v-if=\"currentResource?.isSupportSave\"\n            type=\"primary\"\n            @click=\"currentResource && handleSave(currentResource)\"\n            >转存</el-button\n          >\n        </div>\n      </template>\n    </el-dialog>\n\n    <div v-for=\"group in store.resources\" :key=\"group.id\" class=\"resource-group\">\n      <div\n        :class=\"{ 'group-header': true, 'is-active': group.displayList }\"\n        @click=\"group.displayList = !group.displayList\"\n      >\n        <el-link\n          class=\"group-title\"\n          :href=\"`https://t.me/s/${group.id}`\"\n          target=\"_blank\"\n          :underline=\"false\"\n          @click.stop\n        >\n          <el-image\n            :src=\"getProxyImageUrl(group.channelInfo.channelLogo)\"\n            :fit=\"group.channelInfo.channelLogo ? 'cover' : 'contain'\"\n            class=\"channel-logo\"\n            scroll-container=\"#pc-resources-content\"\n            loading=\"lazy\"\n          />\n          <span>{{ group.channelInfo.name }}</span>\n          <span class=\"item-count\">({{ group.list.length }})</span>\n        </el-link>\n\n        <el-tooltip effect=\"dark\" :content=\"group.displayList ? '收起' : '展开'\" placement=\"top\">\n          <el-button class=\"toggle-btn\" type=\"text\">\n            <el-icon :class=\"{ 'is-active': group.displayList }\">\n              <ArrowDown />\n            </el-icon>\n          </el-button>\n        </el-tooltip>\n      </div>\n\n      <div v-if=\"group.displayList\" class=\"group-content\">\n        <div class=\"card-grid\">\n          <el-card\n            v-for=\"resource in group.list\"\n            :key=\"resource.messageId\"\n            class=\"resource-card-item\"\n            :body-style=\"{ padding: '0' }\"\n          >\n            <div class=\"card-wrapper\">\n              <div class=\"card-cover\">\n                <el-image\n                  loading=\"lazy\"\n                  class=\"cover-image\"\n                  :src=\"getProxyImageUrl(resource.image as string)\"\n                  :fit=\"resource.image ? 'cover' : 'contain'\"\n                  :alt=\"resource.title\"\n                  @click=\"showResourceDetail(resource)\"\n                />\n                <el-tag\n                  class=\"cloud-type\"\n                  :type=\"store.tagColor[resource.cloudType as keyof TagColor]\"\n                  effect=\"dark\"\n                  round\n                  size=\"small\"\n                >\n                  {{ resource.cloudType }}\n                </el-tag>\n              </div>\n\n              <div class=\"card-body\">\n                <el-link\n                  class=\"card-title\"\n                  :href=\"resource.cloudLinks[0]\"\n                  target=\"_blank\"\n                  :underline=\"false\"\n                >\n                  {{ resource.title }}\n                </el-link>\n\n                <div\n                  class=\"card-content\"\n                  @click=\"showResourceDetail(resource)\"\n                  v-html=\"resource.content\"\n                />\n\n                <div v-if=\"resource.tags?.length\" class=\"card-tags\">\n                  <div class=\"tags-list\">\n                    <el-tag\n                      v-for=\"tag in resource.tags\"\n                      :key=\"tag\"\n                      class=\"tag-item\"\n                      @click=\"searchMovieforTag(tag)\"\n                    >\n                      {{ tag }}\n                    </el-tag>\n                  </div>\n                </div>\n\n                <div class=\"card-footer\">\n                  <el-button type=\"primary\" plain @click=\"handleJump(resource)\">跳转</el-button>\n                  <el-button\n                    v-if=\"resource.isSupportSave\"\n                    type=\"primary\"\n                    @click=\"handleSave(resource)\"\n                    >转存</el-button\n                  >\n                </div>\n              </div>\n            </div>\n          </el-card>\n        </div>\n\n        <div class=\"load-more\">\n          <el-button :loading=\"group.loading\" @click=\"handleLoadMore(group.id)\">\n            <el-icon><Plus /></el-icon>\n            加载更多\n          </el-button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useResourceStore } from \"@/stores/resource\";\nimport { ref } from \"vue\";\nimport type { ResourceItem, TagColor } from \"@/types\";\nimport { ArrowDown, Plus } from \"@element-plus/icons-vue\";\nimport { getProxyImageUrl } from \"@/utils/image\";\n\nconst store = useResourceStore();\n\nconst showDetail = ref(false);\nconst currentResource = ref<ResourceItem | null>(null);\n\nconst emit = defineEmits([\"save\", \"loadMore\", \"jump\", \"searchMovieforTag\"]);\n\nconst handleSave = (resource: ResourceItem) => {\n  if (showDetail.value) {\n    showDetail.value = false;\n  }\n  emit(\"save\", resource);\n};\n\nconst handleJump = (resource: ResourceItem) => {\n  emit(\"jump\", resource);\n};\n\nconst showResourceDetail = (resource: ResourceItem) => {\n  currentResource.value = resource;\n  showDetail.value = true;\n};\n\nconst searchMovieforTag = (tag: string) => {\n  emit(\"searchMovieforTag\", tag);\n};\n\nconst handleLoadMore = (channelId: string) => {\n  emit(\"loadMore\", channelId);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.resource-card {\n  position: relative;\n  height: 100%;\n\n  // 资源组\n  .resource-group {\n    background: var(--theme-card-bg);\n    backdrop-filter: var(--theme-blur);\n    -webkit-backdrop-filter: var(--theme-blur);\n    margin-bottom: 24px;\n    border-radius: var(--theme-radius);\n    border: 1px solid rgba(0, 0, 0, 0.08);\n    transition: var(--theme-transition);\n\n    &:last-child {\n      margin-bottom: 100px;\n    }\n  }\n\n  // 组标题\n  .group-header {\n    @include flex-center;\n    justify-content: space-between;\n    padding: 12px 20px;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n    position: sticky;\n    top: 0;\n    background: var(--theme-card-bg);\n    backdrop-filter: var(--theme-blur);\n    -webkit-backdrop-filter: var(--theme-blur);\n    z-index: 10;\n    border-radius: var(--theme-radius);\n    overflow: hidden;\n    cursor: pointer;\n\n    &.is-active {\n      border-radius: var(--theme-radius) var(--theme-radius) 0 0;\n    }\n\n    .group-title {\n      @include flex-center;\n      gap: 12px;\n      font-size: 16px;\n      color: var(--theme-text-primary);\n      transition: var(--theme-transition);\n\n      .channel-logo {\n        width: 32px;\n        height: 32px;\n        border-radius: 50%;\n        overflow: hidden;\n        box-shadow: var(--theme-shadow-sm);\n        margin-right: 8px;\n      }\n\n      .item-count {\n        font-size: 13px;\n        color: var(--theme-text-secondary);\n      }\n\n      &:hover {\n        color: var(--theme-primary);\n        transform: translateY(-1px);\n      }\n    }\n\n    .toggle-btn {\n      width: 32px;\n      height: 32px;\n      padding: 0;\n      color: var(--theme-text-regular);\n      transition: var(--theme-transition);\n\n      .el-icon {\n        font-size: 16px;\n        transition: transform 0.3s ease;\n\n        &.is-active {\n          transform: rotate(180deg);\n        }\n      }\n\n      &:hover {\n        color: var(--theme-primary);\n        transform: translateY(-1px);\n      }\n    }\n  }\n\n  // 组内容\n  .group-content {\n    padding: 20px;\n  }\n\n  // 卡片网格\n  .card-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));\n    gap: 24px;\n    grid-auto-rows: min-content;\n  }\n\n  // 资源卡片\n  .resource-card-item {\n    border-radius: var(--theme-radius);\n    transition: var(--theme-transition);\n    overflow: hidden;\n    max-width: 460px;\n    margin: 0 auto;\n    width: 100%;\n    height: fit-content;\n\n    &:hover {\n      transform: translateY(-2px);\n      box-shadow: var(--theme-shadow);\n    }\n\n    .card-wrapper {\n      display: flex;\n      gap: 20px;\n      padding: 16px;\n      height: 100%;\n    }\n\n    .card-cover {\n      position: relative;\n      width: 120px;\n      height: 180px;\n      flex-shrink: 0;\n\n      .cover-image {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n        border-radius: var(--theme-radius);\n        cursor: pointer;\n        transition: opacity 0.3s ease;\n\n        &:hover {\n          opacity: 0.85;\n        }\n      }\n\n      .cloud-type {\n        position: absolute;\n        top: 8px;\n        left: 8px;\n        z-index: 1;\n      }\n    }\n\n    .card-body {\n      flex: 1;\n      min-width: 0;\n      display: flex;\n      flex-direction: column;\n      gap: 12px;\n\n      .card-title {\n        display: -webkit-box;\n        -webkit-line-clamp: 2;\n        -webkit-box-orient: vertical;\n        overflow: hidden;\n        font-size: 16px;\n        line-height: 1.5;\n        color: var(--theme-text-primary);\n        word-break: break-word;\n        height: 3em;\n        transition: var(--theme-transition);\n\n        &:hover {\n          color: var(--theme-primary);\n        }\n      }\n\n      .card-content {\n        display: -webkit-box;\n        -webkit-line-clamp: 3;\n        -webkit-box-orient: vertical;\n        overflow: hidden;\n        font-size: 14px;\n        line-height: 1.6;\n        color: var(--theme-text-regular);\n        cursor: pointer;\n        transition: color 0.3s ease;\n\n        &:hover {\n          color: var(--theme-text-primary);\n        }\n      }\n\n      .card-tags {\n        margin-top: auto;\n        max-height: 88px;\n        overflow: hidden;\n\n        .tags-label {\n          font-size: 13px;\n          color: var(--theme-text-secondary);\n          margin-right: 8px;\n          display: block;\n          margin-bottom: 8px;\n        }\n\n        .tags-list {\n          display: flex;\n          flex-wrap: wrap;\n          gap: 8px;\n          max-height: 72px;\n          overflow: hidden;\n\n          .tag-item {\n            cursor: pointer;\n            transition: var(--theme-transition);\n            margin: 0;\n            height: 24px;\n\n            &:hover {\n              color: var(--theme-primary);\n              border-color: var(--theme-primary);\n              transform: translateY(-1px);\n            }\n          }\n        }\n      }\n    }\n\n    .card-footer {\n      @include flex-center;\n      justify-content: flex-end;\n      margin-top: 8px;\n\n      .el-button {\n        padding: 6px 16px;\n        font-size: 14px;\n        height: 32px;\n        min-width: 80px;\n\n        &:hover {\n          transform: translateY(-1px);\n          box-shadow: var(--theme-shadow-sm);\n        }\n      }\n    }\n  }\n\n  // 加载更多\n  .load-more {\n    @include flex-center;\n    position: relative;\n    padding: 32px 0 8px;\n    margin-top: 16px;\n\n    &::before {\n      content: \"\";\n      position: absolute;\n      left: 0;\n      right: 0;\n      top: 0;\n      height: 1px;\n      background: linear-gradient(\n        90deg,\n        transparent,\n        var(--el-border-color-lighter) 20%,\n        var(--el-border-color-lighter) 80%,\n        transparent\n      );\n    }\n\n    .el-button {\n      min-width: 160px;\n      height: 40px;\n      border-radius: 20px;\n      font-size: 14px;\n      color: var(--theme-text-regular);\n      background: var(--theme-card-bg);\n      border: 1px solid var(--el-border-color-lighter);\n      transition: var(--theme-transition);\n      position: relative;\n      overflow: hidden;\n\n      &::after {\n        content: \"\";\n        position: absolute;\n        inset: 0;\n        background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);\n        transform: translateX(-100%);\n        transition: transform 0.6s ease;\n      }\n\n      &:hover {\n        color: var(--theme-primary);\n        border-color: var(--theme-primary);\n        background: var(--el-color-primary-light-9);\n\n        &::after {\n          transform: translateX(100%);\n        }\n      }\n\n      &.is-loading {\n        color: var(--theme-text-secondary);\n\n        &::after {\n          display: none;\n        }\n      }\n\n      .el-icon {\n        margin-right: 6px;\n        font-size: 16px;\n      }\n    }\n  }\n\n  // 详情弹窗样式\n  .resource-detail-dialog {\n    :deep(.el-dialog__body) {\n      padding: 20px;\n    }\n\n    .detail-content {\n      display: flex;\n      gap: 24px;\n    }\n\n    .detail-cover {\n      position: relative;\n      width: 200px;\n      flex-shrink: 0;\n\n      .cover-image {\n        width: 100%;\n        height: 300px;\n        border-radius: var(--theme-radius);\n        overflow: hidden;\n      }\n\n      .cloud-type {\n        position: absolute;\n        top: 8px;\n        left: 8px;\n        z-index: 1;\n      }\n    }\n\n    .detail-info {\n      flex: 1;\n      min-width: 0;\n\n      .detail-title {\n        font-size: 18px;\n        margin: 0 0 16px;\n        line-height: 1.5;\n        color: var(--theme-text-primary);\n      }\n\n      .detail-description {\n        font-size: 14px;\n        line-height: 1.6;\n        color: var(--theme-text-regular);\n        margin-bottom: 20px;\n      }\n\n      .detail-tags {\n        .tags-label {\n          font-size: 13px;\n          color: var(--theme-text-secondary);\n          margin-right: 8px;\n        }\n\n        .tags-list {\n          display: flex;\n          flex-wrap: wrap;\n          gap: 8px;\n          margin-top: 8px;\n\n          .tag-item {\n            cursor: pointer;\n            transition: var(--theme-transition);\n\n            &:hover {\n              color: var(--theme-primary);\n              border-color: var(--theme-primary);\n              transform: translateY(-1px);\n            }\n          }\n        }\n      }\n    }\n\n    .dialog-footer {\n      display: flex;\n      justify-content: flex-end;\n      padding-top: 16px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Home/ResourceSelect.vue",
    "content": "<template>\n  <div class=\"resource-select\">\n    <div class=\"select-header\">\n      <div class=\"select-info\">\n        <el-icon><Document /></el-icon>\n        <span>已选择 {{ selectedCount }} 个文件</span>\n        <span v-if=\"totalSize\" class=\"total-size\">({{ formattedFileSize(totalSize) }})</span>\n      </div>\n      <div class=\"header-actions\">\n        <el-button type=\"text\" @click=\"handleSelectAll(!hasSelectedAll)\">\n          {{ hasSelectedAll ? \"取消全选\" : \"全选\" }}\n        </el-button>\n      </div>\n    </div>\n\n    <div class=\"file-list\">\n      <div\n        v-for=\"file in resourceStore.shareInfo.list\"\n        :key=\"file.fileId\"\n        class=\"file-item\"\n        :class=\"{ 'is-checked': isChecked(file.fileId) }\"\n        @click=\"toggleSelect(file)\"\n      >\n        <el-checkbox :model-value=\"isChecked(file.fileId)\" @click.stop>\n          <div class=\"file-info\">\n            <el-icon><Document /></el-icon>\n            <span class=\"file-name\">{{ file.fileName }}</span>\n            <span v-if=\"file.fileSize\" class=\"file-size\">\n              {{ formattedFileSize(file.fileSize) }}\n            </span>\n          </div>\n        </el-checkbox>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useResourceStore } from \"@/stores/resource\";\nimport { formattedFileSize } from \"@/utils/index\";\nimport { computed } from \"vue\";\nimport type { ShareInfo } from \"@/types\";\nimport { Document } from \"@element-plus/icons-vue\";\n\nconst resourceStore = useResourceStore();\n\nconst selectedCount = computed(\n  () => resourceStore.resourceSelect.filter((x) => x.isChecked).length\n);\n\nconst totalSize = computed(() =>\n  resourceStore.resourceSelect\n    .filter((x) => x.isChecked)\n    .reduce((sum, item) => sum + (item.fileSize || 0), 0)\n);\n\nconst totalFiles = computed(() => resourceStore.shareInfo.list.length);\n\nconst hasSelectedAll = computed(() => selectedCount.value === totalFiles.value);\n\nconst isChecked = (fileId: string) => {\n  return resourceStore.resourceSelect.find((x) => x.fileId === fileId)?.isChecked;\n};\n\nconst toggleSelect = (file: ShareInfo) => {\n  let resourceSelect = [...resourceStore.resourceSelect];\n  const item = resourceSelect.find((x) => x.fileId === file.fileId);\n  if (item) {\n    item.isChecked = !item.isChecked;\n    resourceStore.setSelectedResource(resourceSelect);\n  }\n};\n\nconst handleSelectAll = (checked: boolean) => {\n  const resourceSelect = resourceStore.shareInfo.list.map((file) => ({\n    fileId: file.fileId,\n    fileName: file.fileName,\n    fileSize: file.fileSize,\n    isChecked: checked,\n  }));\n  resourceStore.setSelectedResource(resourceSelect);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/responsive.scss\";\n\n.resource-select {\n  min-height: 200px;\n  max-height: 500px;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n\n  .select-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    background: var(--el-fill-color-light);\n    border-radius: var(--theme-radius);\n\n    .select-info {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      color: var(--theme-text-regular);\n      font-size: 14px;\n\n      .el-icon {\n        font-size: 16px;\n      }\n\n      .total-size {\n        color: var(--theme-text-secondary);\n      }\n    }\n\n    .header-actions {\n      display: flex;\n      gap: 16px;\n    }\n\n    .el-button {\n      font-size: 13px;\n      padding: 4px 8px;\n\n      &:not(:disabled):hover {\n        color: var(--theme-primary);\n      }\n    }\n  }\n\n  .file-list {\n    flex: 1;\n    overflow-y: auto;\n    padding: 4px;\n\n    .file-item {\n      padding: 12px 16px;\n      border-radius: var(--theme-radius);\n      cursor: pointer;\n      transition: var(--theme-transition);\n\n      &:hover {\n        background: var(--el-fill-color-light);\n      }\n\n      &.is-checked {\n        background: var(--el-color-primary-light-9);\n      }\n\n      .file-info {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n        color: var(--theme-text-primary);\n        font-size: 14px;\n\n        .el-icon {\n          font-size: 16px;\n          color: var(--theme-text-regular);\n        }\n\n        .file-name {\n          flex: 1;\n          @include text-ellipsis;\n        }\n\n        .file-size {\n          color: var(--theme-text-secondary);\n          font-size: 13px;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Home/ResourceTable.vue",
    "content": "<template>\n  <el-table\n    v-loading=\"store.loading\"\n    class=\"resource-list-table\"\n    :data=\"store.resources\"\n    style=\"width: 100%\"\n    row-key=\"id\"\n    :default-expand-all=\"false\"\n  >\n    <el-table-column type=\"expand\">\n      <template #default=\"props\">\n        <el-table :data=\"props.row.list\" style=\"width: 100%\">\n          <el-table-column label=\"图片\" width=\"80\">\n            <template #default=\"{ row }\">\n              <el-image\n                v-if=\"row.image\"\n                class=\"table-item-image\"\n                :src=\"getProxyImageUrl(row.image as string)\"\n                :fit=\"row.image ? 'cover' : 'contain'\"\n                width=\"30\"\n                height=\"60\"\n              />\n              <el-icon v-else size=\"20\"><Close /></el-icon>\n            </template>\n          </el-table-column>\n          <el-table-column prop=\"title\" label=\"标题\" width=\"280\">\n            <template #default=\"{ row }\">\n              <el-link :href=\"row.cloudLinks[0]\" target=\"_blank\" style=\"font-weight: bold\">\n                {{ row.title }}\n              </el-link>\n            </template>\n          </el-table-column>\n          <el-table-column prop=\"title\" label=\"描述\">\n            <template #default=\"{ row }\">\n              <div class=\"item-description\" v-html=\"row.content\"></div>\n            </template>\n          </el-table-column>\n          <el-table-column prop=\"tags\" label=\"标签\">\n            <template #default=\"{ row }\">\n              <div v-if=\"row.tags.length > 0\" class=\"tags-list\">\n                <span>标签：</span>\n                <el-tag\n                  v-for=\"item in row.tags\"\n                  :key=\"item\"\n                  class=\"resource_tag\"\n                  @click=\"searchMovieforTag(item)\"\n                >\n                  {{ item }}\n                </el-tag>\n              </div>\n              <span v-else>无</span>\n            </template>\n          </el-table-column>\n          <el-table-column label=\"云盘类型\" width=\"120\">\n            <template #default=\"{ row }\">\n              <el-tag :type=\"store.tagColor[row.cloudType as keyof TagColor]\" effect=\"dark\" round>\n                {{ row.cloudType }}\n              </el-tag>\n            </template>\n          </el-table-column>\n          <el-table-column label=\"操作\" width=\"180\">\n            <template #default=\"{ row }\">\n              <el-button type=\"primary\" plain @click=\"handleJump(row)\">跳转</el-button>\n              <el-button v-if=\"row.isSupportSave\" @click=\"handleSave(row)\">转存</el-button>\n            </template>\n          </el-table-column>\n        </el-table>\n        <div class=\"load-more\">\n          <el-button :loading=\"props.row.loading\" @click=\"handleLoadMore(props.row.id)\">\n            加载更多\n          </el-button>\n        </div>\n      </template>\n    </el-table-column>\n    <el-table-column label=\"来源\" prop=\"channel\">\n      <template #default=\"{ row }\">\n        <div class=\"group-header\">\n          <el-image\n            :src=\"getProxyImageUrl(row.channelInfo.channelLogo as string)\"\n            class=\"channel-logo\"\n            :fit=\"row.channelInfo.channelLogo ? 'cover' : 'contain'\"\n            lazy\n          />\n          <span>{{ row.channelInfo.name }}</span>\n          <span class=\"item-count\">({{ row.list.length }})</span>\n        </div>\n      </template>\n    </el-table-column>\n  </el-table>\n</template>\n\n<script setup lang=\"ts\">\nimport { useResourceStore } from \"@/stores/resource\";\nimport type { Resource, TagColor } from \"@/types\";\nimport { getProxyImageUrl } from \"@/utils/image\";\n\nconst store = useResourceStore();\nconst emit = defineEmits([\"save\", \"loadMore\", \"searchMovieforTag\", \"jump\"]);\n\nconst handleSave = (resource: Resource) => {\n  emit(\"save\", resource);\n};\n\nconst handleJump = (resource: Resource) => {\n  emit(\"jump\", resource);\n};\n\n// 添加加载更多处理函数\nconst handleLoadMore = (channelId: string) => {\n  emit(\"loadMore\", channelId);\n};\n\nconst searchMovieforTag = (tag: string) => {\n  emit(\"searchMovieforTag\", tag);\n};\n</script>\n\n<style scoped>\n.resource-list-table {\n  border-radius: 15px;\n}\n\n.group-header {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.channel-logo {\n  width: 20px;\n  height: 20px;\n  margin-right: 10px;\n  border-radius: 50%;\n  overflow: hidden;\n}\n\n.table-item-image {\n  border-radius: 10px;\n  width: 100%;\n}\n\n.item-count {\n  color: #909399;\n  font-size: 0.9em;\n}\n.tags-list {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  flex-wrap: wrap;\n}\n.resource_tag {\n  cursor: pointer;\n  margin-right: 10px;\n  margin-bottom: 5px;\n}\n.item-description {\n  max-width: 100%;\n  margin: 15px 0;\n  -webkit-box-orient: vertical;\n  display: -webkit-box;\n  line-clamp: 2;\n  -webkit-line-clamp: 2;\n  overflow: hidden;\n  white-space: all;\n}\n\n:deep(.el-table__expand-column) {\n  .cell {\n    padding: 0 !important;\n  }\n}\n\n:deep(.el-table__expanded-cell) {\n  padding: 20px !important;\n}\n\n:deep(.el-table__expand-icon) {\n  height: 23px;\n  line-height: 23px;\n}\n.load-more {\n  display: flex;\n  justify-content: center;\n  padding: 16px 0;\n}\n\n.resource-table {\n  position: relative;\n  height: auto;\n  overflow: visible;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/SearchBar.vue",
    "content": "<template>\n  <div class=\"pc-search\">\n    <!-- 搜索区域 -->\n    <div class=\"pc-search__input\">\n      <el-input\n        v-model=\"keyword\"\n        placeholder=\"请输入搜索关键词或输入链接直接解析\"\n        clearable\n        @keyup.enter=\"handleSearch\"\n      >\n        <template #prefix>\n          <el-icon><Search /></el-icon>\n        </template>\n        <template #suffix>\n          <el-icon v-if=\"keyword\" class=\"search-icon\" @click=\"handleSearch\">\n            <ArrowRight />\n          </el-icon>\n        </template>\n      </el-input>\n    </div>\n\n    <!-- 用户操作区 -->\n    <div class=\"pc-search__actions\">\n      <el-tooltip effect=\"dark\" content=\"退出登录\" placement=\"bottom\">\n        <el-button class=\"logout-btn\" type=\"text\" @click=\"handleLogout\">\n          <el-icon><SwitchButton /></el-icon>\n        </el-button>\n      </el-tooltip>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, watch } from \"vue\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { useRoute, useRouter } from \"vue-router\";\nimport { ElMessage } from \"element-plus\";\nimport { Search, ArrowRight, SwitchButton } from \"@element-plus/icons-vue\";\nimport { STORAGE_KEYS } from \"@/constants/storage\";\n\n// 路由相关\nconst route = useRoute();\nconst router = useRouter();\nconst resourcStore = useResourceStore();\n\n// 响应式数据\nconst keyword = ref(\"\");\nconst routeKeyword = computed(() => route.query.keyword as string);\n\n// 退出登录\nconst handleLogout = () => {\n  localStorage.removeItem(STORAGE_KEYS.TOKEN);\n  router.push(\"/login\");\n  ElMessage.success(\"已退出登录\");\n};\n\n// 搜索处理\nconst handleSearch = async () => {\n  const searchText = keyword.value.trim();\n  if (!searchText) {\n    ElMessage.warning(\"请输入搜索内容\");\n    return;\n  }\n\n  // 链接解析处理\n  if (searchText.startsWith(\"http\")) {\n    await resourcStore.parsingCloudLink(searchText);\n    return;\n  }\n\n  // 关键词搜索\n  await resourcStore.searchResources(searchText);\n  if (route.path !== \"/resource\") {\n    router.push(\"/resource\");\n  }\n};\n\n// 监听路由参数变化\nwatch(\n  () => routeKeyword.value,\n  (newKeyword) => {\n    if (newKeyword) {\n      keyword.value = newKeyword;\n      handleSearch();\n    } else {\n      keyword.value = resourcStore.keyword;\n    }\n  }\n);\nwatch(\n  () => resourcStore.keyword,\n  (newKeyword) => {\n    keyword.value = newKeyword;\n  }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.pc-search {\n  @include flex-center;\n  justify-content: space-between;\n  gap: 16px;\n  width: 100%;\n\n  // 搜索输入区域\n  &__input {\n    flex: 1;\n    min-width: 0; // 防止溢出\n\n    :deep(.el-input) {\n      --el-input-height: 44px;\n\n      .el-input__wrapper {\n        @include glass-effect;\n        padding: 0 16px;\n        border-radius: var(--theme-radius);\n        box-shadow:\n          inset 0 0 0 1px rgba(255, 255, 255, 0.1),\n          0 2px 4px rgba(0, 0, 0, 0.05),\n          0 1px 2px rgba(0, 0, 0, 0.1);\n        border: 1px solid rgba(0, 0, 0, 0.08);\n        transition: var(--theme-transition);\n        background: rgba(255, 255, 255, 0.9);\n\n        &:hover {\n          border-color: var(--theme-primary);\n          box-shadow:\n            inset 0 0 0 1px var(--theme-primary),\n            0 4px 8px rgba(0, 0, 0, 0.1);\n        }\n\n        &.is-focus {\n          border-color: var(--theme-primary);\n          box-shadow:\n            inset 0 0 0 1px var(--theme-primary),\n            0 4px 8px rgba(0, 0, 0, 0.1),\n            0 0 0 3px rgba(0, 102, 204, 0.1);\n          background: #fff;\n        }\n      }\n\n      .el-input__inner {\n        font-size: 15px;\n        color: var(--theme-text-primary);\n        height: 42px;\n        line-height: 42px;\n\n        &::placeholder {\n          color: var(--theme-text-secondary);\n        }\n      }\n\n      .el-input__prefix-inner {\n        .el-icon {\n          font-size: 18px;\n          color: var(--theme-text-secondary);\n          margin-right: 8px;\n        }\n      }\n\n      .search-icon {\n        font-size: 18px;\n        cursor: pointer;\n        color: var(--theme-primary);\n        transition: var(--theme-transition);\n        margin-left: 8px;\n\n        &:hover {\n          transform: scale(1.1);\n        }\n      }\n    }\n  }\n\n  // 操作区域\n  &__actions {\n    .logout-btn {\n      @include glass-effect;\n      width: 44px;\n      height: 44px;\n      padding: 0;\n      border-radius: var(--theme-radius);\n      transition: var(--theme-transition);\n\n      .el-icon {\n        font-size: 20px;\n        color: var(--theme-text-regular);\n        transition: var(--theme-transition);\n      }\n\n      &:hover {\n        background: var(--theme-primary);\n        transform: translateY(-2px);\n        box-shadow: var(--theme-shadow-sm);\n\n        .el-icon {\n          color: #fff;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/mobile/FolderSelect.vue",
    "content": "<template>\n  <div class=\"folder-select\">\n    <!-- 面包屑导航 -->\n    <div class=\"folder-select__nav\">\n      <van-cell :border=\"false\" class=\"nav-cell\">\n        <template #title>\n          <div class=\"nav-breadcrumb\">\n            <van-icon name=\"wap-home-o\" class=\"home-icon\" @click=\"handleHomeClick\" />\n            <template v-for=\"(path, index) in currentFolderPath\" :key=\"path.cid\">\n              <van-icon v-if=\"index !== 0\" name=\"arrow\" />\n              <span\n                class=\"path-item\"\n                :class=\"{ 'is-active': index === currentFolderPath.length - 1 }\"\n                @click=\"handleFolderClick(path, index)\"\n              >\n                {{ path.name }}\n              </span>\n            </template>\n          </div>\n        </template>\n      </van-cell>\n    </div>\n\n    <!-- 文件夹列表 -->\n    <div class=\"folder-select__list\">\n      <div v-if=\"resourceStore.loadTree\" class=\"folder-select__loading\">\n        <van-loading type=\"spinner\" vertical>加载中...</van-loading>\n      </div>\n      <van-empty v-if=\"!resourceStore.loadTree && !folders.length\" description=\"暂无文件夹\" />\n      <van-cell-group v-if=\"!resourceStore.loadTree && folders.length\" :border=\"false\">\n        <van-cell\n          v-for=\"folder in folders\"\n          :key=\"folder.cid\"\n          :border=\"false\"\n          clickable\n          @click=\"getList(folder)\"\n        >\n          <template #icon>\n            <van-icon name=\"folder-o\" class=\"folder-icon\" />\n          </template>\n          <template #title>\n            <span class=\"folder-name\">{{ folder.name }}</span>\n          </template>\n          <template #right-icon>\n            <van-icon name=\"arrow\" />\n          </template>\n        </van-cell>\n      </van-cell-group>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, defineProps, onBeforeUnmount } from \"vue\";\nimport { cloud115Api } from \"@/api/cloud115\";\nimport { quarkApi } from \"@/api/quark\";\nimport type { Folder } from \"@/types\";\nimport { type RequestResult } from \"@/types/response\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { showNotify } from \"vant\";\n\nconst props = defineProps({\n  cloudType: {\n    type: String,\n    required: true,\n  },\n});\n\nconst resourceStore = useResourceStore();\nconst folders = ref<Folder[]>([]);\nconst currentFolderPath = ref<Folder[]>([]);\n\nconst emit = defineEmits<{\n  (e: \"select\", currentFolderPath: Folder[] | null): void;\n  (e: \"close\"): void;\n}>();\n\nconst cloudTypeApiMap = {\n  pan115: cloud115Api,\n  quark: quarkApi,\n};\n\n// 返回根目录\nconst handleHomeClick = () => {\n  currentFolderPath.value = [];\n  getList();\n};\n\nconst handleFolderClick = (folder: Folder, index: number) => {\n  currentFolderPath.value = currentFolderPath.value.slice(0, index + 1);\n  getList(folder);\n};\n\nconst getList = async (data?: Folder) => {\n  const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];\n  try {\n    resourceStore.setLoadTree(true);\n    const res: RequestResult<Folder[]> = await api.getFolderList?.(data?.cid || \"0\");\n\n    if (res?.code === 0) {\n      folders.value = res.data || [];\n      if (!data) {\n        currentFolderPath.value = [\n          {\n            name: \"根目录\",\n            cid: \"0\",\n          },\n        ];\n      } else if (!currentFolderPath.value.find((p) => p.cid === data.cid)) {\n        currentFolderPath.value.push(data);\n      }\n      emit(\"select\", currentFolderPath.value);\n    } else {\n      throw new Error(res.message);\n    }\n  } catch (error) {\n    showNotify({\n      type: \"danger\",\n      message: error instanceof Error ? error.message : \"获取目录失败\",\n    });\n    currentFolderPath.value = [];\n    folders.value = [];\n    emit(\"select\", null);\n    emit(\"close\");\n  } finally {\n    resourceStore.setLoadTree(false);\n  }\n};\n\n// 初始化加载\ngetList();\n\n// 组件销毁前重置状态\nonBeforeUnmount(() => {\n  currentFolderPath.value = [];\n  folders.value = [];\n  emit(\"select\", null);\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.folder-select {\n  position: relative;\n  height: 100%;\n  background: var(--theme-other_background);\n  display: flex;\n  flex-direction: column;\n\n  &__nav {\n    flex-shrink: 0;\n    border-bottom: 0.5px solid #f5f5f5;\n    background: var(--theme-other_background);\n\n    .nav-cell {\n      padding: 12px 16px;\n      min-height: 24px;\n    }\n\n    .nav-breadcrumb {\n      display: flex;\n      align-items: center;\n      flex-wrap: wrap;\n      gap: 4px;\n      font-size: 14px;\n      line-height: 1.4;\n      min-height: 20px;\n    }\n\n    .home-icon {\n      font-size: 16px;\n      color: var(--theme-theme);\n      margin-right: 4px;\n    }\n\n    .path-item {\n      color: #666;\n      padding: 2px 4px;\n      border-radius: 4px;\n\n      &.is-active {\n        color: var(--theme-theme);\n        font-weight: 500;\n      }\n\n      &:active {\n        background-color: #f5f5f5;\n      }\n    }\n  }\n\n  &__list {\n    flex: 1;\n    overflow-y: auto;\n    padding: 8px 0;\n    position: relative;\n    min-height: 200px;\n    display: flex;\n    flex-direction: column;\n\n    .van-empty {\n      flex: 1;\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n    }\n\n    .van-cell-group {\n      flex: 1;\n    }\n  }\n\n  &__loading {\n    position: absolute;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: rgba(255, 255, 255, 0.9);\n    z-index: 0;\n\n    .van-loading {\n      padding: 16px 24px;\n      background: rgba(0, 0, 0, 0.7);\n      border-radius: 8px;\n      color: #fff;\n    }\n  }\n\n  .folder-icon {\n    font-size: 20px;\n    color: var(--theme-theme);\n    margin-right: 8px;\n  }\n\n  .folder-name {\n    font-size: 15px;\n    color: var(--theme-color);\n  }\n}\n\n// 深度修改 Vant 组件样式\n:deep(.van-cell) {\n  padding: 12px 16px;\n\n  &::after {\n    display: none;\n  }\n\n  &:active {\n    background-color: #f5f5f5;\n  }\n}\n\n:deep(.van-empty) {\n  padding: 32px 0;\n  margin: 0;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/mobile/ResourceCard.vue",
    "content": "<template>\n  <div class=\"resource-card\">\n    <div v-for=\"item in dataList\" :key=\"item.id\" class=\"resource-card__item\">\n      <!-- 内容区域 -->\n      <div class=\"item__content\">\n        <!-- 左侧图片 -->\n        <div class=\"content__image\">\n          <van-image\n            :src=\"getProxyImageUrl(item.image as string)\"\n            :fit=\"item.image ? 'cover' : 'contain'\"\n            lazy-load\n          />\n          <!-- 来源标签移到图片左上角 -->\n          <van-tag class=\"image__tag\" :color=\"getTagColor(item.cloudType)\" round>\n            {{ item.cloudType }}\n          </van-tag>\n        </div>\n\n        <!-- 右侧信息 -->\n        <div class=\"content__info\">\n          <!-- 标题 -->\n          <div class=\"info__title\" @click=\"copyUrl(item.cloudLinks[0])\">\n            {{ item.title }}\n          </div>\n\n          <!-- 描述 - 添加展开收起功能 -->\n          <div\n            class=\"info__desc\"\n            :class=\"{\n              'is-expanded': expandedItems[(item.messageId || '') + (item.channelId || '')],\n            }\"\n            @click=\"toggleExpand((item.messageId || '') + (item.channelId || ''))\"\n            v-html=\"item.content\"\n          />\n\n          <!-- 底部区域：标签 -->\n          <div class=\"info__footer\">\n            <div v-if=\"item.tags?.length\" class=\"info__tags\">\n              <van-tag\n                v-for=\"tag in item.tags\"\n                :key=\"tag\"\n                type=\"primary\"\n                plain\n                round\n                @click.stop=\"searchMovieforTag(tag)\"\n              >\n                {{ tag }}\n              </van-tag>\n            </div>\n\n            <!-- 转存按钮 -->\n            <div class=\"info__action\">\n              <van-button type=\"primary\" size=\"mini\" round plain @click=\"handleJump(item)\">\n                跳转\n              </van-button>\n              <van-button\n                v-if=\"item.isSupportSave\"\n                type=\"primary\"\n                size=\"mini\"\n                round\n                @click=\"handleSave(item)\"\n                >转存</van-button\n              >\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from \"vue\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { showNotify } from \"vant\";\nimport type { ResourceItem } from \"@/types\";\nimport { getProxyImageUrl } from \"@/utils/image\";\n\n// Props 定义\nconst props = defineProps<{\n  currentChannelId: string;\n}>();\n\n// 事件定义\nconst emit = defineEmits<{\n  (e: \"save\", resource: ResourceItem): void;\n  (e: \"jump\", resource: ResourceItem): void;\n  (e: \"searchMovieforTag\", tag: string): void;\n}>();\n\n// 状态管理\nconst store = useResourceStore();\n\n// 计算属性\nconst dataList = computed(() => {\n  const channel = store.resources.find((item) => item.id === props.currentChannelId);\n  return channel?.list || [];\n});\n\n// 标签颜色映射\nconst getTagColor = (type?: string) => {\n  const colorMap: Record<string, string> = {\n    pan115: \"#07c160\",\n    quark: \"#1989fa\",\n  };\n  return colorMap[type || \"\"] || \"#ff976a\";\n};\n\n// 方法定义\nconst handleSave = (resource: ResourceItem) => {\n  emit(\"save\", resource);\n};\n\nconst handleJump = (resource: ResourceItem) => {\n  emit(\"jump\", resource);\n};\n\nconst copyUrl = async (url: string) => {\n  try {\n    await navigator.clipboard.writeText(url);\n    showNotify({\n      type: \"success\",\n      message: \"链接已复制到剪贴板\",\n      duration: 1500,\n    });\n  } catch (err) {\n    const input = document.createElement(\"input\");\n    input.value = url;\n    document.body.appendChild(input);\n    input.select();\n    document.execCommand(\"copy\");\n    document.body.removeChild(input);\n\n    showNotify({\n      type: \"success\",\n      message: \"链接已复制到剪贴板\",\n      duration: 1500,\n    });\n  }\n};\n\nconst searchMovieforTag = (tag: string) => {\n  emit(\"searchMovieforTag\", tag);\n};\n\n// 展开状态管理\nconst expandedItems = ref<Record<string, boolean>>({});\n\n// 切换展开状态\nconst toggleExpand = (id: string) => {\n  expandedItems.value[id] = !expandedItems.value[id];\n};\n</script>\n\n<style lang=\"scss\" scoped>\n// 文本省略混入 - 移到最前面\n@mixin text-ellipsis($lines) {\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: $lines;\n  overflow: hidden;\n}\n\n.resource-card {\n  padding: 5px 10px;\n\n  &__item {\n    margin-bottom: 12px;\n    background: var(--theme-other_background);\n    border-radius: var(--border-radius-lg);\n    overflow: hidden;\n  }\n}\n\n.item {\n  &__content {\n    display: flex;\n    gap: 16px;\n    padding: 16px;\n  }\n}\n\n.content {\n  &__image {\n    position: relative; // 为标签定位\n    flex-shrink: 0;\n    width: 100px;\n    height: 140px;\n    border-radius: var(--border-radius-sm);\n    overflow: hidden;\n    background: var(--van-gray-2);\n\n    :deep(.van-image) {\n      width: 100%;\n      height: 100%;\n    }\n\n    .image__tag {\n      position: absolute;\n      top: 8px;\n      left: 8px;\n      font-size: 10px;\n      padding: 0 6px;\n    }\n  }\n\n  &__info {\n    flex: 1;\n    min-width: 0;\n    display: flex;\n    flex-direction: column;\n    gap: var(--spacing-xs);\n  }\n}\n\n.info {\n  &__title {\n    font-size: 15px;\n    font-weight: 500;\n    line-height: 1.4;\n    color: var(--theme-color);\n    @include text-ellipsis(2);\n\n    &:active {\n      opacity: 0.7;\n    }\n  }\n\n  &__desc {\n    position: relative;\n    font-size: 13px;\n    line-height: 1.6;\n    color: var(--van-gray-7);\n    @include text-ellipsis(3);\n    margin: 4px 0;\n    cursor: pointer;\n    transition: all 0.3s;\n\n    &.is-expanded {\n      -webkit-line-clamp: 8;\n    }\n\n    &::after {\n      content: \"展开\";\n      position: absolute;\n      right: 0;\n      bottom: 0;\n      padding: 0 4px;\n      font-size: 12px;\n      color: var(--theme-theme);\n      background: var(--theme-other_background);\n    }\n\n    &.is-expanded::after {\n      content: \"收起\";\n    }\n  }\n\n  &__footer {\n    display: flex;\n    flex-direction: column;\n    gap: var(--spacing-xs);\n    margin-top: auto;\n  }\n\n  &__tags {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 6px;\n\n    :deep(.van-tag) {\n      font-size: 11px;\n      padding: 0 8px;\n    }\n  }\n\n  &__action {\n    display: flex;\n    justify-content: flex-end;\n    padding: 4px 0;\n\n    .van-button {\n      font-size: 13px;\n      height: 32px;\n      padding: 0 20px;\n\n      :deep(.van-button__text) {\n        font-weight: 500;\n        font-size: 14px;\n      }\n\n      &:active {\n        opacity: 0.8;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/mobile/ResourceSelect.vue",
    "content": "<template>\n  <div class=\"resource-select\">\n    <van-checkbox-group v-model=\"selectedResourceIds\">\n      <van-cell-group :border=\"false\">\n        <van-cell\n          v-for=\"item in resourceStore.shareInfo.list\"\n          :key=\"item.fileId\"\n          class=\"resource-item\"\n          :border=\"false\"\n          center\n          @click=\"handleItemClick(item.fileId)\"\n        >\n          <template #title>\n            <div class=\"resource-item__content\">\n              <van-icon name=\"folder-o\" class=\"content__icon\" />\n              <div class=\"content__info\">\n                <span class=\"info__name\">{{ item.fileName }}</span>\n                <span v-if=\"item.fileSize\" class=\"info__size\">\n                  {{ formattedFileSize(item.fileSize) }}\n                </span>\n              </div>\n            </div>\n          </template>\n          <template #right-icon>\n            <van-checkbox\n              :name=\"item.fileId\"\n              class=\"resource-item__checkbox\"\n              @click.stop=\"handleItemClick(item.fileId)\"\n            />\n          </template>\n        </van-cell>\n      </van-cell-group>\n    </van-checkbox-group>\n\n    <!-- 空状态 -->\n    <van-empty v-if=\"!resourceStore.shareInfo.list?.length\" description=\"暂无可选资源\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watch } from \"vue\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { formattedFileSize } from \"@/utils/index\";\n\nconst resourceStore = useResourceStore();\nconst selectedResourceIds = ref<string[]>([]);\n\n// 初始化选中状态\nselectedResourceIds.value = resourceStore.resourceSelect\n  .filter((x) => x.isChecked)\n  .map((x) => x.fileId);\n\n// 监听选中状态变化\nwatch(selectedResourceIds, (newIds) => {\n  const newResourceSelect = [...resourceStore.resourceSelect];\n  newResourceSelect.forEach((x) => {\n    x.isChecked = newIds.includes(x.fileId);\n  });\n  resourceStore.setSelectedResource(newResourceSelect);\n});\n\n// 添加点击处理函数\nconst handleItemClick = (fileId: string) => {\n  const index = selectedResourceIds.value.indexOf(fileId);\n  if (index === -1) {\n    selectedResourceIds.value.push(fileId);\n  } else {\n    selectedResourceIds.value.splice(index, 1);\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n// 工具类\n@mixin text-ellipsis {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.resource-select {\n  height: 100%;\n  background: var(--theme-other_background);\n  width: 100%;\n  overflow-x: hidden;\n\n  .resource-item {\n    position: relative;\n\n    &__content {\n      display: flex;\n      align-items: flex-start;\n      gap: 8px;\n      padding: 8px 0;\n      margin-right: 40px;\n\n      .content__icon {\n        flex-shrink: 0;\n        font-size: 20px;\n        color: var(--theme-theme);\n        margin-top: 2px;\n      }\n\n      .content__info {\n        flex: 1;\n        min-width: 0;\n        display: flex;\n        flex-direction: column;\n        gap: 2px;\n\n        .info__name {\n          font-size: 15px;\n          line-height: 1.4;\n          color: var(--van-text-color);\n          word-break: break-all;\n          white-space: normal;\n          display: -webkit-box;\n          -webkit-line-clamp: 2;\n          -webkit-box-orient: vertical;\n          overflow: hidden;\n        }\n\n        .info__size {\n          font-size: 13px;\n          color: var(--van-gray-6);\n          @include text-ellipsis;\n        }\n      }\n    }\n\n    &__checkbox {\n      position: absolute;\n      right: 16px;\n      top: 50%;\n      transform: translateY(-50%);\n\n      :deep(.van-checkbox__icon) {\n        font-size: 18px;\n        cursor: pointer;\n\n        .van-icon {\n          border-radius: 2px;\n          transition: all 0.2s;\n        }\n      }\n    }\n\n    &:active {\n      background-color: var(--van-active-color);\n    }\n  }\n}\n\n// 深度修改 Vant 组件样式\n:deep(.van-cell) {\n  align-items: flex-start;\n  padding: 0 16px;\n  width: 100%;\n  box-sizing: border-box;\n  min-height: 60px;\n  position: relative;\n\n  &::after {\n    display: none;\n  }\n\n  .van-cell__title {\n    flex: 1;\n    min-width: 0;\n  }\n}\n\n:deep(.van-checkbox__icon--checked) {\n  .van-icon {\n    background-color: var(--theme-theme);\n    border-color: var(--theme-theme);\n  }\n}\n\n:deep(.van-empty) {\n  padding: 32px 0;\n  background: transparent;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/constants/project.ts",
    "content": "export const PROJECT_NAME = \"Cloudsaver\";\nexport const PROJECT_GITHUB = \"https://github.com/jiangrui1994/cloudsaver\";\n"
  },
  {
    "path": "frontend/src/constants/storage.ts",
    "content": "export const STORAGE_KEYS = {\n  USERNAME: \"saved_username\",\n  PASSWORD: \"saved_password\",\n  TOKEN: \"token\",\n} as const;\n"
  },
  {
    "path": "frontend/src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module \"*.vue\" {\n  import type { DefineComponent } from \"vue\";\n  const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;\n  export default component;\n}\n\ninterface ImportMetaEnv {\n  readonly VITE_API_BASE_URL: string;\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv;\n}\n"
  },
  {
    "path": "frontend/src/main.ts",
    "content": "import { createApp } from \"vue\";\nimport { createPinia } from \"pinia\";\nimport ElementPlus from \"element-plus\";\nimport \"element-plus/dist/index.css\";\nimport * as ElementPlusIconsVue from \"@element-plus/icons-vue\";\nimport zhCn from \"element-plus/es/locale/lang/zh-cn\";\nimport { isMobileDevice } from \"@/utils/index\";\nimport App from \"./App.vue\";\nimport { Lazyload } from \"vant\";\nimport \"vant/es/notify/style\";\nimport \"vant/es/dialog/style\";\nimport \"@/styles/responsive.scss\";\nimport \"@/styles/common.scss\";\n\nimport router from \"./router/index\";\n\nconst app = createApp(App);\n\nfor (const [key, component] of Object.entries(ElementPlusIconsVue)) {\n  app.component(key, component);\n}\n\napp.use(createPinia());\napp.use(Lazyload);\napp.use(router);\napp.use(ElementPlus, {\n  locale: zhCn,\n});\n\napp.mount(\"#app\");\n\nconst setRootFontSize = () => {\n  const isMobile = isMobileDevice();\n  if (!isMobile) {\n    return;\n  } // PC端不干预\n  const clientWidth = document.documentElement.clientWidth;\n  const baseSize = clientWidth / 7.5; // 按750px设计稿\n  document.documentElement.style.fontSize = baseSize + \"px\";\n};\n\n// 初始化执行\nsetRootFontSize();\n// 监听窗口变化\nwindow.addEventListener(\"resize\", setRootFontSize);\n"
  },
  {
    "path": "frontend/src/router/index.ts",
    "content": "import { createRouter, createWebHistory } from \"vue-router\";\nimport mobileRoutes from \"./mobile-routes\";\nimport pcRoutes from \"./pc-routes\";\nimport { isMobileDevice } from \"@/utils/index\";\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [...(isMobileDevice() ? mobileRoutes : pcRoutes)],\n});\n\nexport default router;\n"
  },
  {
    "path": "frontend/src/router/mobile-routes.ts",
    "content": "import type { RouteRecordRaw } from \"vue-router\";\nconst routes: RouteRecordRaw[] = [\n  {\n    path: \"/\",\n    name: \"home\",\n    component: () => import(\"@/views/mobile/Home.vue\"),\n    redirect: \"/resource\",\n    children: [\n      {\n        path: \"/resource\",\n        name: \"resource\",\n        component: () => import(\"@/views/mobile/ResourceList.vue\"),\n      },\n      {\n        path: \"/douban\",\n        name: \"douban\",\n        component: () => import(\"@/views/mobile/Douban.vue\"),\n      },\n      {\n        path: \"/setting\",\n        name: \"setting\",\n        component: () => import(\"@/views/mobile/Setting.vue\"),\n      },\n      {\n        path: \"/thanks\",\n        name: \"thanks\",\n        redirect: \"/resource\",\n      },\n    ],\n  },\n  {\n    path: \"/login\",\n    name: \"login\",\n    component: () => import(\"@/views/mobile/Login.vue\"),\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "frontend/src/router/pc-routes.ts",
    "content": "import type { RouteRecordRaw } from \"vue-router\";\nconst routes: RouteRecordRaw[] = [\n  {\n    path: \"/\",\n    name: \"home\",\n    component: () => import(\"@/views/Home.vue\"),\n    redirect: \"/resource\",\n    children: [\n      {\n        path: \"/resource\",\n        name: \"resource\",\n        component: () => import(\"@/views/ResourceList.vue\"),\n      },\n      {\n        path: \"/douban\",\n        name: \"douban\",\n        component: () => import(\"@/views/Douban.vue\"),\n      },\n      {\n        path: \"/setting\",\n        name: \"setting\",\n        component: () => import(\"@/views/Setting.vue\"),\n      },\n      {\n        path: \"/thanks\",\n        name: \"thanks\",\n        component: () => import(\"@/views/Thanks.vue\"),\n      },\n    ],\n  },\n  {\n    path: \"/login\",\n    name: \"login\",\n    component: () => import(\"@/views/pc/Login.vue\"),\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "frontend/src/stores/douban.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { doubanApi } from \"@/api/douban\";\nimport { HotListItem } from \"@/types/douban\";\nimport { ElMessage } from \"element-plus\";\n\ninterface StoreType {\n  hotList: HotListItem[];\n  loading: boolean;\n  currentParams: CurrentParams;\n}\n\ninterface CurrentParams {\n  type: string;\n  tag?: string;\n}\n\nexport const useDoubanStore = defineStore(\"douban\", {\n  state: (): StoreType => ({\n    hotList: [],\n    loading: false,\n    currentParams: {\n      type: \"movie\",\n      tag: \"热门\",\n    },\n  }),\n\n  actions: {\n    async getHotList() {\n      this.loading = true;\n      try {\n        const params = {\n          type: this.currentParams.type,\n          tag: this.currentParams.tag || \"热门\",\n          page_limit: \"20\",\n          page_start: \"0\",\n        };\n        const result = await doubanApi.getHotList(params);\n        if (result && result.length > 0) {\n          this.hotList = result;\n        } else {\n          console.log(\"获取热门列表失败\");\n          ElMessage.warning(\"获取热门列表失败\");\n        }\n      } catch (error) {\n        ElMessage.error(error || \"获取热门列表失败\");\n      } finally {\n        this.loading = false;\n      }\n    },\n    setCurrentParams(currentParams: CurrentParams) {\n      this.currentParams = currentParams;\n      this.getHotList();\n    },\n  },\n});\n"
  },
  {
    "path": "frontend/src/stores/index.ts",
    "content": "import { defineStore } from \"pinia\";\n\ninterface StoreType {\n  scrollTop: boolean;\n}\n\nexport const useStore = defineStore(\"global\", {\n  state: (): StoreType => ({\n    scrollTop: true,\n  }),\n\n  actions: {\n    setScrollTop(top: boolean) {\n      this.scrollTop = top;\n    },\n  },\n});\n"
  },
  {
    "path": "frontend/src/stores/resource.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { cloud115Api } from \"@/api/cloud115\";\nimport { resourceApi } from \"@/api/resource\";\nimport { quarkApi } from \"@/api/quark\";\nimport type {\n  Resource,\n  ShareInfoResponse,\n  ShareInfo,\n  ResourceItem,\n  GetShareInfoParams,\n  SaveFileParams,\n  ShareFileInfoAndFolder,\n} from \"@/types\";\nimport { ElMessage } from \"element-plus\";\n\ninterface StorageListObject {\n  list: Resource[];\n  lastUpdateTime?: string;\n}\n\nconst lastResource = (\n  localStorage.getItem(\"last_resource_list\")\n    ? JSON.parse(localStorage.getItem(\"last_resource_list\") as string)\n    : { list: [] }\n) as StorageListObject;\n\n// 定义云盘驱动配置类型\ninterface CloudDriveConfig {\n  name: string;\n  type: string;\n  regex: RegExp;\n  api: {\n    getShareInfo: (params: GetShareInfoParams) => Promise<ShareInfoResponse>;\n    saveFile: (params: SaveFileParams) => Promise<{ code: number; message?: string }>;\n  };\n  parseShareCode: (match: RegExpMatchArray) => GetShareInfoParams;\n  getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => SaveFileParams;\n}\n\n// 云盘类型配置\nexport const CLOUD_DRIVES: CloudDriveConfig[] = [\n  {\n    name: \"115网盘\",\n    type: \"pan115\",\n    regex: /(?:115|anxia|115cdn)\\.com\\/s\\/([^?]+)(?:\\?password=([^&#]+))?/,\n    api: {\n      getShareInfo: (params: GetShareInfoParams) => cloud115Api.getShareInfo(params),\n      saveFile: async (params: SaveFileParams) => {\n        return await cloud115Api.saveFile(params);\n      },\n    },\n    parseShareCode: (match: RegExpMatchArray) => ({\n      shareCode: match[1],\n      receiveCode: match[2] || \"\",\n    }),\n    getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({\n      shareCode: shareInfoAndFolder.shareCode || \"\",\n      receiveCode: shareInfoAndFolder.receiveCode || \"\",\n      fileId: shareInfoAndFolder.shareInfo.list[0].fileId,\n      folderId: shareInfoAndFolder.folderId,\n      fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || \"\"),\n    }),\n  },\n  {\n    name: \"夸克网盘\",\n    type: \"quark\",\n    regex: /pan\\.quark\\.cn\\/s\\/([a-zA-Z0-9]+)/,\n    api: {\n      getShareInfo: (params) => quarkApi.getShareInfo(params),\n      saveFile: async (params: SaveFileParams) => {\n        return await quarkApi.saveFile(params);\n      },\n    },\n    parseShareCode: (match: RegExpMatchArray) => ({ shareCode: match[1] }),\n    getSaveParams: (shareInfoAndFolder: ShareFileInfoAndFolder) => ({\n      fids: shareInfoAndFolder.shareInfo.list.map((item: { fileId?: string }) => item.fileId || \"\"),\n      fidTokens: shareInfoAndFolder.shareInfo.list.map(\n        (item: { fileIdToken?: string }) => item.fileIdToken || \"\"\n      ),\n      folderId: shareInfoAndFolder.folderId,\n      shareCode: shareInfoAndFolder.shareInfo.pwdId || \"\",\n      receiveCode: shareInfoAndFolder.shareInfo.stoken || \"\",\n    }),\n  },\n];\n\nexport const useResourceStore = defineStore(\"resource\", {\n  state: () => ({\n    tagColor: {\n      baiduPan: \"primary\",\n      weiyun: \"info\",\n      aliyun: \"warning\",\n      pan115: \"danger\",\n      quark: \"success\",\n    },\n    keyword: \"\",\n    resources: lastResource.list,\n    lastUpdateTime: lastResource.lastUpdateTime || \"\",\n    shareInfo: {} as ShareInfoResponse,\n    resourceSelect: [] as ShareInfo[],\n    loading: false,\n    backupPlan: false,\n    loadTree: false,\n  }),\n\n  actions: {\n    setLoadTree(loadTree: boolean) {\n      this.loadTree = loadTree;\n    },\n    // 搜索资源\n    async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise<void> {\n      this.loading = true;\n      if (!isLoadMore) this.resources = [];\n      try {\n        let lastMessageId = \"\";\n        if (isLoadMore) {\n          const list = this.resources.find((x) => x.id === channelId)?.list || [];\n          lastMessageId = list[list.length - 1].messageId || \"\";\n          if (list[list.length - 1].isLastMessage) {\n            ElMessage.warning(\"没有更多了~\");\n            return;\n          }\n          if (!lastMessageId) {\n            ElMessage.error(\"当次搜索源不支持加载更多\");\n            return;\n          }\n          keyword = this.keyword;\n        }\n        let { data = [] } = await resourceApi.search(keyword || \"\", channelId, lastMessageId);\n        this.keyword = keyword || \"\";\n        data = data\n          .filter((item) => item.list.length > 0)\n          .map((x) => ({\n            ...x,\n            list: x.list.map((item) => ({\n              ...item,\n              isSupportSave: CLOUD_DRIVES.some((drive) => drive.regex.test(item.cloudLinks[0])),\n            })),\n          }));\n        console.log(data);\n        if (isLoadMore) {\n          const findedIndex = this.resources.findIndex((item) => item.id === data[0]?.id);\n          if (findedIndex !== -1) {\n            this.resources[findedIndex].list.push(...data[0].list);\n          }\n          if (data.length === 0) {\n            const list = this.resources.find((item) => item.id === channelId)?.list;\n            list && list[list.length - 1] && (list[list.length - 1]!.isLastMessage = true);\n            ElMessage.warning(\"没有更多了~\");\n          }\n        } else {\n          this.resources = data.map((item, index) => ({ ...item, displayList: index === 0 }));\n          if (!keyword) {\n            // 获取当前时间字符串 用于存储到本地\n            this.lastUpdateTime = new Date().toLocaleString();\n            localStorage.setItem(\n              \"last_resource_list\",\n              JSON.stringify({ list: this.resources, lastUpdateTime: this.lastUpdateTime })\n            );\n          }\n          if (this.resources.length === 0) {\n            ElMessage.warning(\"未搜索到相关资源\");\n          }\n        }\n      } catch (error) {\n        console.log(error);\n        this.handleError(\"搜索失败，请重试\", null);\n      } finally {\n        this.loading = false;\n      }\n    },\n\n    // 设置选择资源\n    async setSelectedResource(resourceSelect: ShareInfo[]) {\n      this.resourceSelect = resourceSelect;\n    },\n\n    // 转存资源\n    async saveResource(resource: ResourceItem, folderId: string): Promise<void> {\n      const savePromises: Promise<void>[] = [];\n      CLOUD_DRIVES.forEach((drive) => {\n        if (resource.cloudLinks.some((link) => drive.regex.test(link))) {\n          savePromises.push(this.saveResourceToDrive(resource, folderId, drive));\n        }\n      });\n      await Promise.all(savePromises);\n    },\n\n    // 保存资源到网盘\n    async saveResourceToDrive(\n      resource: ResourceItem,\n      folderId: string,\n      drive: CloudDriveConfig\n    ): Promise<void> {\n      const link = resource.cloudLinks.find((link) => drive.regex.test(link));\n      if (!link) return;\n\n      const match = link.match(drive.regex);\n      if (!match) throw new Error(\"链接解析失败\");\n      const parsedCode = drive.parseShareCode(match);\n\n      const shareInfo = {\n        ...this.shareInfo,\n        list: this.resourceSelect.filter((x) => x.isChecked),\n      };\n      console.log(shareInfo);\n\n      const params = drive.getSaveParams({\n        shareInfo,\n        ...parsedCode,\n        folderId,\n      });\n      const result = await drive.api.saveFile(params);\n\n      if (result.code === 0) {\n        ElMessage.success(`${drive.name} 转存成功`);\n      } else {\n        ElMessage.error(result.message);\n      }\n    },\n\n    // 解析云盘链接\n    async parsingCloudLink(url: string): Promise<void> {\n      this.loading = true;\n      this.resources = [];\n      try {\n        const matchedDrive = CLOUD_DRIVES.find((drive) => drive.regex.test(url));\n        if (!matchedDrive) throw new Error(\"不支持的网盘链接\");\n\n        const match = url.match(matchedDrive.regex);\n        if (!match) throw new Error(\"链接解析失败\");\n\n        const parsedCode = matchedDrive.parseShareCode(match);\n        const shareInfo = await matchedDrive.api.getShareInfo(parsedCode);\n        if (shareInfo?.list?.length) {\n          this.resources = [\n            {\n              id: \"\",\n              channelInfo: {\n                name: \"自定义搜索\",\n                channelLogo: \"\",\n                channelId: \"\",\n              },\n              displayList: true,\n              list: [\n                {\n                  id: \"1\",\n                  title: shareInfo.list.map((item) => item.fileName).join(\", \"),\n                  cloudLinks: [url],\n                  cloudType: matchedDrive.type,\n                  channel: matchedDrive.name,\n                  pubDate: \"\",\n                  isSupportSave: true,\n                },\n              ],\n            },\n          ];\n        } else {\n          throw new Error(\"解析失败，请检查链接是否正确\");\n        }\n      } catch (error) {\n        this.handleError(\"解析失败，请重试\", error);\n      } finally {\n        this.loading = false;\n      }\n    },\n\n    // 获取资源列表并选择\n    async getResourceListAndSelect(resource: ResourceItem): Promise<boolean> {\n      this.setSelectedResource([]);\n      const { cloudType } = resource;\n      const drive = CLOUD_DRIVES.find((x) => x.type === cloudType);\n      if (!drive) {\n        return false;\n      }\n      const link = resource.cloudLinks.find((link) => drive.regex.test(link));\n      if (!link) return false;\n\n      const match = link.match(drive.regex);\n      if (!match) throw new Error(\"链接解析失败\");\n\n      const parsedCode = drive.parseShareCode(match);\n      this.setLoadTree(true);\n      let shareInfo = await drive.api.getShareInfo(parsedCode);\n      console.log(shareInfo);\n      this.setLoadTree(false);\n      if (shareInfo) {\n        shareInfo = {\n          ...shareInfo,\n          ...parsedCode,\n        };\n        this.shareInfo = shareInfo;\n        this.setSelectedResource(this.shareInfo.list.map((x) => ({ ...x, isChecked: true })));\n        return true;\n      } else {\n        ElMessage.error(\"获取资源信息失败,请先检查cookie!\");\n        return false;\n      }\n    },\n\n    // 统一错误处理\n    handleError(message: string, error: unknown): void {\n      console.error(message, error);\n      ElMessage.error(error instanceof Error ? error.message : message);\n    },\n  },\n});\n"
  },
  {
    "path": "frontend/src/stores/userSetting.ts",
    "content": "import { defineStore } from \"pinia\";\nimport type {\n  UserSettingStore,\n  GlobalSettingAttributes,\n  UserSettingAttributes,\n} from \"@/types/user\";\nimport { settingApi } from \"@/api/setting\";\nimport { ElMessage } from \"element-plus\";\n\nexport const useUserSettingStore = defineStore(\"user\", {\n  state: (): UserSettingStore => ({\n    globalSetting: null,\n    userSettings: {\n      cloud115Cookie: \"\",\n      quarkCookie: \"\",\n    },\n    displayStyle: (localStorage.getItem(\"display_style\") as \"table\" | \"card\") || \"card\",\n    imagesSource: (localStorage.getItem(\"images_source\") as \"proxy\" | \"local\") || \"proxy\",\n  }),\n\n  actions: {\n    async getSettings() {\n      const { data } = await settingApi.getSetting();\n      if (data) {\n        this.globalSetting = data.globalSetting;\n        this.userSettings = data.userSettings;\n      }\n    },\n\n    async saveSettings(settings: {\n      globalSetting?: GlobalSettingAttributes | null;\n      userSettings: UserSettingAttributes;\n    }) {\n      try {\n        await settingApi.saveSetting(settings);\n        await this.getSettings();\n      } catch (error) {\n        console.log(error);\n        throw error;\n      }\n    },\n\n    setDisplayStyle(style: \"table\" | \"card\") {\n      this.displayStyle = style;\n      localStorage.setItem(\"display_style\", style);\n      ElMessage.success(`切换成功，当前为${style === \"table\" ? \"列表\" : \"卡片\"}模式`);\n    },\n\n    setImagesSource(source: \"proxy\" | \"local\") {\n      this.imagesSource = source;\n      localStorage.setItem(\"images_source\", source);\n      ElMessage.success(`切换成功，图片模式当前为${source === \"proxy\" ? \"代理\" : \"直连\"}模式`);\n    },\n  },\n});\n"
  },
  {
    "path": "frontend/src/styles/common.scss",
    "content": "// 颜色系统\n:root {\n  // 主题色\n  --theme-primary: #0066cc;\n  --theme-primary-hover: #0256ac;\n  --theme-success: #28cd41;\n  --theme-warning: #ff9f0a;\n  --theme-error: #ff3b30;\n\n  // 中性色\n  --theme-bg: #f5f7fa;\n  --theme-card-bg: rgba(255, 255, 255, 0.8);\n  --theme-text-primary: #000000;\n  --theme-text-regular: #424242;\n  --theme-text-secondary: #6e6e6e;\n\n  // 特效\n  --theme-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);\n  --theme-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n  --theme-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);\n\n  // 圆角\n  --theme-radius-sm: 8px;\n  --theme-radius: 12px;\n  --theme-radius-lg: 16px;\n\n  // 模糊效果\n  --theme-blur: blur(12px);\n\n  // 动画\n  --theme-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n// Mixins\n@mixin glass-effect {\n  background: var(--theme-card-bg);\n  backdrop-filter: var(--theme-blur);\n  border: 1px solid rgba(255, 255, 255, 0.2);\n}\n\n@mixin flex-center {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n@mixin text-overflow {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n// 通用动画\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n"
  },
  {
    "path": "frontend/src/styles/global.scss",
    "content": ":root {\n  --theme-color: #3e3e3e;\n  --theme-theme: #133ab3;\n  --theme-background: #fafafa;\n  --theme-other_background: #ffffff;\n}\n\ninput {\n  border: none;\n  outline: none;\n}\n"
  },
  {
    "path": "frontend/src/styles/mobile.scss",
    "content": "/* 移动端通用样式类 */\n.mobile-page {\n    padding: var(--spacing-base);\n    min-height: 100vh;\n    background-color: var(--theme-background);\n}\n\n.mobile-card {\n    background: var(--theme-other_background);\n    border-radius: var(--border-radius-lg);\n    padding: var(--spacing-base);\n    margin-bottom: var(--spacing-base);\n}\n\n.mobile-title {\n    font-size: var(--font-size-xl);\n    font-weight: bold;\n    margin-bottom: var(--spacing-base);\n}\n\n.mobile-text {\n    font-size: var(--font-size-base);\n    line-height: 1.6;\n    color: var(--theme-color);\n}\n\n.mobile-button {\n    width: 100%;\n    height: 40px;\n    font-size: var(--font-size-lg);\n    border-radius: 20px;\n}\n\n.mobile-form {\n    .van-field {\n        padding: var(--spacing-base);\n\n        &__label {\n            font-size: var(--font-size-base);\n        }\n\n        &__control {\n            font-size: var(--font-size-base);\n        }\n    }\n}"
  },
  {
    "path": "frontend/src/styles/responsive.scss",
    "content": "@mixin text-ellipsis {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n// 响应式布局工具类\n@mixin mobile {\n  @media screen and (max-width: 768px) {\n    @content;\n  }\n}\n\n@mixin tablet {\n  @media screen and (min-width: 769px) and (max-width: 1024px) {\n    @content;\n  }\n}\n\n@mixin desktop {\n  @media screen and (min-width: 1025px) {\n    @content;\n  }\n}\n\n// 通用样式变量\n:root {\n  // 字体大小 - 整体缩小约25%\n  --font-size-xs: 20px; // 原24px\n  --font-size-sm: 22px; // 原26px\n  --font-size-base: 24px; // 原28px\n  --font-size-lg: 28px; // 原32px\n  --font-size-xl: 32px; // 原36px\n\n  // 间距 - 也相应缩小\n  --spacing-xs: 6px; // 原8px\n  --spacing-sm: 10px; // 原12px\n  --spacing-base: 14px; // 原16px\n  --spacing-lg: 20px; // 原24px\n  --spacing-xl: 28px; // 原32px\n\n  // 圆角 - 适当调整\n  --border-radius-sm: 6px; // 原8px\n  --border-radius-base: 10px; // 原12px\n  --border-radius-lg: 14px; // 原16px\n  --border-radius-xl: 20px; // 原24px\n\n  // 移动端特殊变量\n  @include mobile {\n    --font-size-base: 14px;\n    --spacing-base: 12px;\n  }\n}\n\n// 移动端适配\n@media screen and (max-width: 768px) {\n  :root {\n    // 间距\n    --spacing-xs: 3px;\n    --spacing-sm: 5px;\n    --spacing-base: 7px;\n    --spacing-lg: 10px;\n    --spacing-xl: 14px;\n\n    // 字体大小\n    --font-size-xs: 10px;\n    --font-size-sm: 11px;\n    --font-size-base: 12px;\n    --font-size-lg: 14px;\n    --font-size-xl: 16px;\n\n    // 圆角\n    --border-radius-sm: 3px;\n    --border-radius-base: 5px;\n    --border-radius-lg: 7px;\n  }\n}\n"
  },
  {
    "path": "frontend/src/types/douban.ts",
    "content": "export interface HotListParams {\n  type: string;\n  tag?: string;\n  page_limit?: string;\n  page_start?: string;\n}\nexport interface HotListItem {\n  cover: string;\n  cover_x: number;\n  cover_y: number;\n  episodes_info: string;\n  id: string;\n  is_new: boolean;\n  playable: boolean;\n  rate: string;\n  title: string;\n  url: string;\n}\n"
  },
  {
    "path": "frontend/src/types/globals.d.ts",
    "content": "declare global {\n  interface Location {\n    // 根据你的需求定义 location 的属性和方法\n    pathname: string;\n    search: string;\n    hash: string;\n    host: string;\n    // 其他属性和方法...\n  }\n  interface Window {\n    location: Location;\n  }\n}\n\nexport {};\n"
  },
  {
    "path": "frontend/src/types/index.ts",
    "content": "export interface ResourceItem {\n  id: string;\n  title: string;\n  channel: string;\n  channelId?: string;\n  image?: string;\n  cloudLinks: string[];\n  tags?: string[];\n  content?: string;\n  pubDate: string;\n  cloudType: string;\n  messageId?: string;\n  isLastMessage?: boolean;\n  isSupportSave?: boolean;\n}\n\nexport interface Resource {\n  list: ResourceItem[];\n  displayList?: boolean;\n  loading?: boolean;\n  channelInfo: {\n    channelId: string;\n    name: string;\n    channelLogo: string;\n  };\n  id: string;\n}\n\nexport interface ShareInfo {\n  fileId: string;\n  fileName: string;\n  fileSize?: number;\n  fileIdToken?: string;\n  isChecked?: boolean;\n}\n\nexport interface ShareInfoItem {\n  fileId: string;\n  fileName: string;\n  fileSize?: number;\n  fileIdToken?: string;\n}\n\nexport interface ShareInfoResponse {\n  list: ShareInfoItem[];\n  fileSize?: number;\n  pwdId?: string;\n  stoken?: string;\n}\n\nexport interface ShareFileInfoAndFolder {\n  shareInfo: ShareInfoResponse;\n  folderId: string;\n  shareCode: string;\n  receiveCode?: string;\n}\n\nexport interface Folder {\n  cid: string;\n  name: string;\n  path?: Folder[];\n}\n\nexport interface SaveFileParams {\n  shareCode: string; // 分享code\n  receiveCode?: string; // 分享文件的密码\n  folderId: string; // 文件夹id\n  fids: string[]; // 存储文件id\n  fidTokens?: string[]; // 存储文件token\n}\n\nexport interface GetShareInfoParams {\n  shareCode: string;\n  receiveCode?: string;\n}\n\nexport interface ApiResponse<T = unknown> {\n  success: boolean;\n  data?: T;\n  error?: string;\n}\n\nexport interface Save115FileParams {\n  shareCode: string;\n  receiveCode: string;\n  fileId: string;\n  folderId: string;\n}\n\nexport interface SaveQuarkFileParams {\n  fid_list: string[];\n  fid_token_list: string[];\n  to_pdir_fid: string;\n  pwd_id: string;\n  stoken: string;\n  pdir_fid: string;\n  scene: string;\n}\n\nexport interface TagColor {\n  baiduPan: string;\n  weiyun: string;\n  aliyun: string;\n  pan115: string;\n  quark: string;\n}\n\nexport interface GlobalSettingAttributes {\n  httpProxyHost: string;\n  httpProxyPort: number | string;\n  isProxyEnabled: boolean;\n  AdminUserCode: number;\n  CommonUserCode: number;\n}\nexport interface UserSettingAttributes {\n  cloud115Cookie: string;\n  quarkCookie: string;\n}\n"
  },
  {
    "path": "frontend/src/types/response.ts",
    "content": "export type RequestErrorCode = -1 | 400 | 401 | 402 | 403 | 500 | 501;\n\nexport interface RequestSuccess<T> {\n  code: 0;\n  data: T;\n  message: string;\n}\n\nexport interface RequestError<T> {\n  code: RequestErrorCode;\n  message: string;\n  data?: T;\n}\n\nexport type RequestResult<T> = RequestSuccess<T> | RequestError<T>;\n"
  },
  {
    "path": "frontend/src/types/user.ts",
    "content": "export interface GlobalSettingAttributes {\n  httpProxyHost: string;\n  httpProxyPort: string | number;\n  isProxyEnabled: boolean;\n  AdminUserCode: number;\n  CommonUserCode: number;\n}\n\nexport interface UserSettingAttributes {\n  cloud115Cookie: string;\n  quarkCookie: string;\n}\n\nexport interface UserSettingStore {\n  globalSetting: GlobalSettingAttributes | null;\n  userSettings: UserSettingAttributes;\n  displayStyle: \"table\" | \"card\";\n  imagesSource: \"proxy\" | \"local\";\n}\n"
  },
  {
    "path": "frontend/src/utils/device.ts",
    "content": "export const isMobile = () => {\n  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n};\n\nexport const isTablet = () => {\n  const userAgent = navigator.userAgent.toLowerCase();\n  return /(ipad|tablet|(android(?!.*mobile))|(windows(?!.*phone)(.*touch))|kindle|playbook|silk|(puffin(?!.*(IP|AP|WP))))/.test(\n    userAgent\n  );\n};\n"
  },
  {
    "path": "frontend/src/utils/image.ts",
    "content": "import { useUserSettingStore } from \"@/stores/userSetting\";\nimport defaultImage from \"@/assets/images/default.png\";\n\nexport const getProxyImageUrl = (originalUrl: string): string => {\n  const userStore = useUserSettingStore();\n  if (!originalUrl) return defaultImage;\n  return userStore.imagesSource === \"proxy\"\n    ? `/tele-images/?url=${encodeURIComponent(originalUrl)}`\n    : originalUrl;\n};\n"
  },
  {
    "path": "frontend/src/utils/index.ts",
    "content": "export const formattedFileSize = (size: number): string => {\n  if (size < 1024 * 1024) {\n    return `${(size / 1024).toFixed(2)}KB`;\n  }\n  if (size < 1024 * 1024 * 1024) {\n    return `${(size / 1024 / 1024).toFixed(2)}MB`;\n  }\n  return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;\n};\n\nexport function isMobileDevice() {\n  return (\n    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||\n    window.innerWidth <= 768\n  );\n}\n\nexport function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): T {\n  let lastTime = 0;\n\n  return function (this: any, ...args: Parameters<T>) {\n    const now = Date.now();\n\n    if (now - lastTime >= delay) {\n      fn.apply(this, args);\n      lastTime = now;\n    }\n  } as T;\n}\n"
  },
  {
    "path": "frontend/src/utils/request.ts",
    "content": "import axios, { AxiosResponse, AxiosRequestConfig } from \"axios\";\nimport { ElMessage } from \"element-plus\";\nimport { isMobileDevice } from \"@/utils/index\";\nimport { showNotify } from \"vant\";\nimport { RequestResult } from \"../types/response\";\nimport { STORAGE_KEYS } from \"@/constants/storage\";\n\nconst errorMessage = (message: string) => {\n  if (isMobileDevice()) {\n    console.log(message);\n    showNotify({\n      type: \"danger\",\n      message,\n    });\n    return;\n  }\n  ElMessage.error(message);\n};\n\nconst axiosInstance = axios.create({\n  baseURL: import.meta.env.VITE_API_BASE_URL as string,\n  timeout: 16000,\n  withCredentials: true,\n  headers: {\n    \"Content-Type\": \"application/json\",\n  },\n});\n\nfunction isLoginAndRedirect(url: string) {\n  return url.includes(\"/api/user/login\") || url.includes(\"/api/user/register\");\n}\n\naxiosInstance.interceptors.request.use(\n  (config) => {\n    const token = localStorage.getItem(STORAGE_KEYS.TOKEN);\n    if (token) {\n      config.headers.Authorization = `Bearer ${token}`;\n    } else if (!isLoginAndRedirect(config.url || \"\")) {\n      errorMessage(\"请先登录\");\n      window.location.href = \"/login\";\n    }\n    return config;\n  },\n  (error) => {\n    return Promise.reject(error);\n  }\n);\n\naxiosInstance.interceptors.response.use(\n  (response: AxiosResponse) => {\n    const res = response.data;\n    return res;\n  },\n  (error) => {\n    if (error.response.status === 401) {\n      errorMessage(\"登录过期，请重新登录\");\n      localStorage.removeItem(STORAGE_KEYS.TOKEN);\n      window.location.href = \"/login\";\n      return Promise.reject(new Error(\"登录过期，请重新登录\"));\n    }\n    errorMessage(error.response.statusText);\n    return Promise.reject(new Error(error.response.statusText));\n  }\n);\n\nconst request = {\n  get: <T>(url: string, config?: AxiosRequestConfig): Promise<RequestResult<T>> => {\n    return axiosInstance.get(url, { ...config });\n  },\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  post: <T, D = any>(\n    url: string,\n    data: D,\n    config?: AxiosRequestConfig\n  ): Promise<RequestResult<T>> => {\n    return axiosInstance.post(url, data, { ...config });\n  },\n  put: axiosInstance.put,\n  delete: axiosInstance.delete,\n};\n\nexport default request;\n"
  },
  {
    "path": "frontend/src/views/Douban.vue",
    "content": "<template>\n  <div class=\"douban-page\">\n    <div class=\"movie-wall\">\n      <div v-for=\"movie in doubanStore.hotList\" :key=\"movie.id\" class=\"movie-item\">\n        <div class=\"movie-poster\">\n          <el-image\n            class=\"movie-poster-img\"\n            :src=\"movie.cover\"\n            fit=\"cover\"\n            lazy\n            :alt=\"movie.title\"\n            hide-on-click-modal\n            :preview-src-list=\"[movie.cover]\"\n          />\n          <div class=\"movie-rate\">\n            {{ movie.rate }}\n          </div>\n          <div class=\"movie-poster-hover\" @click=\"searchMovie(movie.title)\">\n            <div class=\"movie-search\">\n              <el-icon class=\"search_icon\" size=\"28px\"><Search /></el-icon>\n            </div>\n          </div>\n        </div>\n        <div class=\"movie-info\">\n          <el-link :href=\"movie.url\" target=\"_blank\" :underline=\"false\" class=\"movie-title\">{{\n            movie.title\n          }}</el-link>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from \"vue\";\nimport { useRouter, useRoute } from \"vue-router\";\nimport { useDoubanStore } from \"@/stores/douban\";\ninterface CurrentParams {\n  type: string;\n  tag?: string;\n}\nconst router = useRouter();\nconst route = useRoute();\n\nconst routeParams = computed((): CurrentParams => ({ ...route.query }) as unknown as CurrentParams);\nconst doubanStore = useDoubanStore();\nif (routeParams.value) {\n  doubanStore.setCurrentParams(routeParams.value);\n}\n\nwatch(\n  () => routeParams.value,\n  () => {\n    console.log(routeParams.value);\n    doubanStore.setCurrentParams(routeParams.value);\n  }\n);\n\nconst searchMovie = (title: string) => {\n  router.push({ path: \"/\", query: { keyword: title } });\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n@import \"@/styles/responsive.scss\";\n\n.douban-page {\n  height: calc(100vh - 180px);\n  overflow-y: auto;\n\n  &::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: rgba(0, 0, 0, 0.2);\n    border-radius: 4px;\n\n    &:hover {\n      background: rgba(0, 0, 0, 0.3);\n    }\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n}\n\n.movie-wall {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, 200px);\n  justify-content: space-between;\n  gap: 20px;\n  padding: 4px;\n}\n\n.movie-item {\n  width: 200px;\n  background: var(--theme-card-bg);\n  border-radius: var(--theme-radius);\n  box-shadow: var(--theme-shadow);\n  box-sizing: border-box;\n  padding: 12px;\n  transition: var(--theme-transition);\n\n  &:hover {\n    transform: translateY(-2px);\n    box-shadow: var(--theme-shadow-lg);\n  }\n}\n\n.movie-poster-img {\n  width: 100%;\n  height: 220px;\n  object-fit: cover;\n  border-radius: var(--theme-radius);\n  overflow: hidden;\n}\n\n.movie-info {\n  padding: 12px 0 4px;\n  text-align: center;\n  width: 100%;\n\n  .movie-title {\n    display: block;\n    font-size: 16px;\n    font-weight: bold;\n    color: var(--theme-text-primary);\n    transition: var(--theme-transition);\n    @include text-ellipsis;\n    max-width: 100%;\n    line-height: 1.2;\n\n    &:hover {\n      color: var(--theme-primary);\n    }\n  }\n}\n\n.movie-poster {\n  width: 100%;\n  height: 220px;\n  position: relative;\n  overflow: hidden;\n  border-radius: var(--theme-radius);\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0;\n}\n\n.movie-poster-hover {\n  opacity: 0;\n  transition: opacity 0.3s ease;\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  background-color: rgba(0, 0, 0, 0.5);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  backdrop-filter: blur(2px);\n}\n\n.movie-poster:hover .movie-poster-hover {\n  opacity: 1;\n}\n\n.movie-rate {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  background: var(--theme-primary);\n  color: white;\n  padding: 0px 8px;\n  border-radius: var(--theme-radius-sm);\n  font-size: 14px;\n}\n\n.movie-search {\n  color: white;\n  border-radius: var(--theme-radius);\n  font-size: 14px;\n  cursor: pointer;\n  transition: transform 0.2s ease;\n\n  &:hover {\n    transform: scale(1.1);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/Home.vue",
    "content": "<template>\n  <div class=\"pc-home\" :class=\"{ 'is-loading': resourcStore.loading }\">\n    <!-- 主布局容器 -->\n    <el-container class=\"pc-home__container\">\n      <!-- 侧边栏 -->\n      <el-aside width=\"220px\" class=\"pc-home__aside\">\n        <aside-menu />\n      </el-aside>\n\n      <!-- 主内容区 -->\n      <el-container class=\"pc-home__main\">\n        <!-- 顶部搜索栏 -->\n        <el-header class=\"pc-home__header\" :class=\"{ 'is-scrolled': !store.scrollTop }\">\n          <search-bar />\n        </el-header>\n\n        <!-- 内容区域 -->\n        <el-main class=\"pc-home__content\">\n          <div class=\"content-wrapper\">\n            <router-view v-slot=\"{ Component }\">\n              <transition name=\"fade\" mode=\"out-in\">\n                <component :is=\"Component\" />\n              </transition>\n            </router-view>\n          </div>\n        </el-main>\n      </el-container>\n    </el-container>\n\n    <!-- 全局加载 -->\n    <div v-if=\"resourcStore.loading\" class=\"pc-home__loading\">\n      <el-icon class=\"is-loading\"><Loading /></el-icon>\n      <span class=\"loading-text\">加载中...</span>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted } from \"vue\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { useStore } from \"@/stores/index\";\nimport { useUserSettingStore } from \"@/stores/userSetting\";\nimport { throttle } from \"@/utils/index\";\nimport { Loading } from \"@element-plus/icons-vue\";\nimport \"element-plus/es/components/loading/style/css\";\nimport AsideMenu from \"@/components/AsideMenu.vue\";\nimport SearchBar from \"@/components/SearchBar.vue\";\n\n// 状态管理\nconst resourcStore = useResourceStore();\nconst store = useStore();\nconst settingStore = useUserSettingStore();\n\n// 初始化设置\nonMounted(() => {\n  settingStore.getSettings();\n  window.addEventListener(\"scroll\", handleScroll);\n});\n\nonUnmounted(() => {\n  window.removeEventListener(\"scroll\", handleScroll);\n});\n\n// 滚动处理\nconst handleScroll = throttle(() => {\n  const scrollTop = window.scrollY;\n  store.setScrollTop(scrollTop <= 50);\n}, 100);\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.pc-home {\n  position: relative;\n  height: 100vh;\n  background: var(--theme-bg);\n  color: var(--theme-text-primary);\n\n  // 主容器\n  &__container {\n    height: 100%;\n  }\n\n  // 侧边栏\n  &__aside {\n    background: var(--theme-card-bg);\n    backdrop-filter: var(--theme-blur);\n    border-right: 1px solid rgba(0, 0, 0, 0.1);\n    overflow: hidden;\n    transition: var(--theme-transition);\n\n    &:hover {\n      box-shadow: var(--theme-shadow);\n    }\n  }\n\n  // 主内容区\n  &__main {\n    position: relative;\n    width: 100%;\n    display: flex;\n    flex-direction: column;\n    padding: 0;\n    height: 100%;\n  }\n\n  // 顶部搜索栏\n  &__header {\n    position: sticky;\n    top: 0;\n    z-index: 10;\n    height: auto;\n    padding: 16px;\n    background: var(--theme-card-bg);\n    backdrop-filter: var(--theme-blur);\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n    transition: var(--theme-transition);\n\n    &.is-scrolled {\n      padding: 12px;\n      box-shadow: var(--theme-shadow-sm);\n    }\n  }\n\n  // 内容区域\n  &__content {\n    flex: 1;\n    padding: 20px;\n    height: 0;\n\n    .content-wrapper {\n      height: 100%;\n    }\n  }\n\n  // 加载状态\n  &__loading {\n    @include flex-center;\n    position: fixed;\n    inset: 0;\n    z-index: 2000;\n    flex-direction: column;\n    gap: 16px;\n    background: rgba(255, 255, 255, 0.1);\n    backdrop-filter: blur(18px);\n    -webkit-backdrop-filter: blur(18px);\n    animation: fadeIn 0.3s ease;\n\n    .loading-text {\n      color: var(--theme-text-primary);\n      font-size: 14px;\n      text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);\n    }\n\n    .is-loading {\n      font-size: 24px;\n      color: var(--theme-primary);\n      animation: rotating 2s linear infinite;\n      filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.1));\n    }\n  }\n}\n\n// 加载动画\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    backdrop-filter: blur(0);\n  }\n  to {\n    opacity: 1;\n    backdrop-filter: blur(8px);\n  }\n}\n\n// 路由过渡动画\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n@keyframes rotating {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/ResourceList.vue",
    "content": "<template>\n  <div class=\"pc-resources\">\n    <!-- 头部工具栏 -->\n    <div class=\"pc-resources__header\">\n      <div class=\"header__left\">\n        <el-tooltip effect=\"dark\" content=\"点击获取最新资源\" placement=\"bottom\">\n          <el-button class=\"refresh-btn\" type=\"text\" @click=\"refreshResources\">\n            <el-icon><Refresh /></el-icon>\n            <span>最新资源</span>\n            <span class=\"update-time\"> (上次刷新时间：{{ resourceStore.lastUpdateTime }}) </span>\n          </el-button>\n        </el-tooltip>\n      </div>\n\n      <div class=\"header__right\">\n        <el-tooltip\n          effect=\"dark\"\n          :content=\"\n            userStore.imagesSource === 'local' ? '图片切换到代理模式' : '图片切换到直连模式'\n          \"\n          placement=\"bottom\"\n        >\n          <el-button\n            type=\"text\"\n            class=\"view-toggle\"\n            @click=\"\n              userStore.setImagesSource(userStore.imagesSource === 'proxy' ? 'local' : 'proxy')\n            \"\n          >\n            <el-icon>\n              <component :is=\"userStore.imagesSource === 'proxy' ? 'Guide' : 'Location'\" />\n            </el-icon>\n          </el-button>\n        </el-tooltip>\n        <el-tooltip\n          effect=\"dark\"\n          :content=\"userStore.displayStyle === 'card' ? '切换到列表视图' : '切换到卡片视图'\"\n          placement=\"bottom\"\n        >\n          <el-button\n            type=\"text\"\n            class=\"view-toggle\"\n            @click=\"setDisplayStyle(userStore.displayStyle === 'card' ? 'table' : 'card')\"\n          >\n            <el-icon>\n              <component :is=\"userStore.displayStyle === 'card' ? 'Menu' : 'Grid'\" />\n            </el-icon>\n          </el-button>\n        </el-tooltip>\n      </div>\n    </div>\n\n    <!-- 资源列表 -->\n    <div id=\"pc-resources-content\" ref=\"contentRef\" class=\"pc-resources__content\">\n      <component\n        :is=\"userStore.displayStyle === 'table' ? ResourceTable : ResourceCard\"\n        v-if=\"resourceStore.resources.length > 0\"\n        @load-more=\"handleLoadMore\"\n        @jump=\"handleJump\"\n        @search-moviefor-tag=\"searchMovieforTag\"\n        @save=\"handleSave\"\n      />\n\n      <!-- 空状态 -->\n      <div v-if=\"resourceStore.resources.length === 0\" class=\"pc-resources__empty\">\n        <el-empty :image-size=\"200\">\n          <template #description>\n            <p class=\"empty-text\">暂无资源</p>\n            <el-tooltip effect=\"dark\" content=\"点击获取最新资源\" placement=\"top\">\n              <el-button type=\"primary\" @click=\"refreshResources\">\n                <el-icon><Refresh /></el-icon>\n                <span>刷新资源</span>\n              </el-button>\n            </el-tooltip>\n          </template>\n        </el-empty>\n      </div>\n    </div>\n\n    <!-- 返回顶部 -->\n    <el-backtop :bottom=\"40\" :right=\"40\" target=\".pc-resources__content\">\n      <div class=\"pc-resources__backtop\">\n        <el-icon><ArrowUp /></el-icon>\n      </div>\n    </el-backtop>\n\n    <!-- 保存对话框 -->\n    <el-dialog\n      v-if=\"currentResource\"\n      v-model=\"saveDialogVisible\"\n      :title=\"saveDialogMap[saveDialogStep].title\"\n      width=\"580px\"\n      destroy-on-close\n    >\n      <template #header=\"{ titleId }\">\n        <div class=\"dialog-header\">\n          <h3 :id=\"titleId\">\n            <div class=\"title-main\">\n              <el-tag\n                :type=\"resourceStore.tagColor[currentResource.cloudType as keyof TagColor]\"\n                effect=\"dark\"\n                round\n              >\n                {{ currentResource.cloudType }}\n              </el-tag>\n              {{ saveDialogMap[saveDialogStep].title }}\n            </div>\n            <div class=\"title-sub\">\n              <span class=\"resource-title\" :title=\"currentResource.title\">\n                {{ currentResource.title }}\n              </span>\n              <span\n                v-if=\"resourceStore.shareInfo.fileSize && saveDialogStep === 1\"\n                class=\"file-size\"\n              >\n                ({{ formattedFileSize(resourceStore.shareInfo.fileSize) }})\n              </span>\n            </div>\n          </h3>\n        </div>\n      </template>\n\n      <div v-loading=\"resourceStore.loadTree\">\n        <resource-select\n          v-if=\"saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length\"\n          :cloud-type=\"currentResource.cloudType\"\n        />\n        <folder-select\n          v-if=\"saveDialogVisible && saveDialogStep === 2\"\n          :cloud-type=\"currentResource.cloudType\"\n          @select=\"handleFolderSelect\"\n          @close=\"saveDialogVisible = false\"\n        />\n      </div>\n\n      <template #footer>\n        <div class=\"dialog-footer\">\n          <el-button @click=\"saveDialogVisible = false\">取消</el-button>\n          <el-button type=\"primary\" @click=\"handleConfirmClick\">\n            {{ saveDialogMap[saveDialogStep].buttonText }}\n          </el-button>\n        </div>\n      </template>\n    </el-dialog>\n\n    <!-- 加载状态 -->\n    <div v-if=\"resourceStore.loading\" class=\"pc-resources__loading\">\n      <div class=\"loading-text\">加载中...</div>\n      <div class=\"is-loading\">\n        <el-icon><Loading /></el-icon>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from \"vue\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { useUserSettingStore } from \"@/stores/userSetting\";\nimport FolderSelect from \"@/components/Home/FolderSelect.vue\";\nimport ResourceSelect from \"@/components/Home/ResourceSelect.vue\";\nimport ResourceTable from \"@/components/Home/ResourceTable.vue\";\nimport { formattedFileSize } from \"@/utils/index\";\nimport type { ResourceItem, TagColor } from \"@/types\";\nimport { onMounted, onBeforeUnmount } from \"vue\";\nimport ResourceCard from \"@/components/Home/ResourceCard.vue\";\nimport { useRouter } from \"vue-router\";\nimport { ElMessage } from \"element-plus\";\nimport { ArrowUp } from \"@element-plus/icons-vue\";\nconst router = useRouter();\n\nconst resourceStore = useResourceStore();\nconst userStore = useUserSettingStore();\nconst saveDialogVisible = ref(false);\nconst currentResource = ref<ResourceItem | null>(null);\nconst currentFolderId = ref<string | null>(null);\nconst saveDialogStep = ref<1 | 2>(1);\n\nconst refreshResources = async () => {\n  resourceStore.searchResources(\"\", false);\n};\n\nconst saveDialogMap = {\n  1: {\n    title: \"选择资源\",\n    buttonText: \"下一步\",\n  },\n  2: {\n    title: \"选择保存目录\",\n    buttonText: \"保存到此目录\",\n  },\n};\n\nconst handleSave = async (resource: ResourceItem) => {\n  currentResource.value = resource;\n  saveDialogVisible.value = true;\n  saveDialogStep.value = 1;\n  if (!(await resourceStore.getResourceListAndSelect(currentResource.value))) {\n    saveDialogVisible.value = false;\n  }\n};\n\nconst handleFolderSelect = async (folderId: string) => {\n  if (!currentResource.value) return;\n  currentFolderId.value = folderId;\n};\n\nconst handleConfirmClick = async () => {\n  if (saveDialogStep.value === 1) {\n    const selectedFiles = resourceStore.resourceSelect.filter((x) => x.isChecked);\n    if (selectedFiles.length === 0) {\n      ElMessage.warning(\"请选择要保存的资源\");\n      return;\n    }\n    saveDialogStep.value = 2;\n  } else {\n    handleSaveBtnClick();\n  }\n};\n\nconst handleSaveBtnClick = async () => {\n  if (!currentResource.value || !currentFolderId.value) return;\n  saveDialogVisible.value = false;\n  await resourceStore.saveResource(currentResource.value, currentFolderId.value);\n};\nconst setDisplayStyle = (style: string) => {\n  userStore.setDisplayStyle(style as \"card\" | \"table\");\n};\n// 添加加载更多处理函数\nconst handleLoadMore = (channelId: string) => {\n  resourceStore.searchResources(\"\", true, channelId);\n};\n\nconst handleJump = (resource: ResourceItem) => {\n  window.open(resource.cloudLinks[0], \"_blank\");\n};\n\nconst searchMovieforTag = (tag: string) => {\n  router.push({ path: \"/resource\", query: { keyword: tag } });\n};\n// 页面进入 设置缓存的数据源\nonMounted(() => {\n  const lastResourceList = localStorage.getItem(\"last_resource_list\");\n  if (lastResourceList) {\n    resourceStore.resources = JSON.parse(lastResourceList).list;\n  }\n});\n\n// 页面销毁 清除搜索词\nonBeforeUnmount(() => {\n  resourceStore.keyword = \"\";\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n@import \"@/styles/responsive.scss\";\n\n.pc-resources {\n  // 整体容器\n  position: relative;\n  width: 100%;\n\n  // 头部工具栏\n  &__header {\n    @include glass-effect;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    min-height: 48px;\n    padding: 0 20px;\n    margin-bottom: 16px;\n    border-radius: var(--theme-radius);\n    border: 1px solid rgba(0, 0, 0, 0.08);\n    transition: var(--theme-transition);\n\n    &:hover {\n      border-color: var(--theme-primary);\n      box-shadow: var(--theme-shadow-sm);\n    }\n\n    .header__left {\n      display: flex;\n      align-items: center;\n      flex-wrap: wrap;\n      gap: 12px;\n      padding: 8px 0;\n\n      .refresh-btn {\n        @include flex-center;\n        gap: 8px;\n        color: var(--theme-text-regular);\n        transition: var(--theme-transition);\n        white-space: nowrap;\n\n        .el-icon {\n          font-size: 18px;\n          color: var(--theme-primary);\n        }\n\n        .update-time {\n          margin-left: 4px;\n          font-size: 13px;\n          color: var(--theme-text-secondary);\n          white-space: nowrap;\n        }\n\n        &:hover {\n          color: var(--theme-primary);\n          transform: translateY(-1px);\n        }\n      }\n    }\n\n    .header__right {\n      display: flex;\n      align-items: center;\n      gap: 12px;\n      padding: 8px 0;\n\n      .view-toggle {\n        width: 36px;\n        height: 36px;\n        padding: 0;\n        color: var(--theme-text-regular);\n        border-radius: var(--theme-radius);\n        transition: var(--theme-transition);\n\n        .el-icon {\n          font-size: 18px;\n        }\n\n        &:hover {\n          color: var(--theme-primary);\n          background: rgba(0, 102, 204, 0.05);\n          transform: translateY(-1px);\n        }\n      }\n    }\n  }\n\n  // 内容区域\n  &__content {\n    position: relative;\n    width: 100%;\n    height: calc(100vh - 180px);\n    overflow-y: auto;\n\n    // 资源列表组件样式覆盖\n    :deep(.resource-table),\n    :deep(.resource-card) {\n      height: 100%;\n\n      // 自定义滚动条\n      &::-webkit-scrollbar {\n        width: 8px;\n        height: 8px;\n      }\n\n      &::-webkit-scrollbar-thumb {\n        background: rgba(0, 0, 0, 0.2);\n        border-radius: 4px;\n\n        &:hover {\n          background: rgba(0, 0, 0, 0.3);\n        }\n      }\n\n      &::-webkit-scrollbar-track {\n        background: transparent;\n      }\n    }\n  }\n\n  // 加载状态\n  &__loading {\n    @include glass-effect;\n    @include flex-center;\n    position: fixed;\n    inset: 0;\n    z-index: 2000;\n    flex-direction: column;\n    gap: 16px;\n    background: rgba(255, 255, 255, 0.3);\n    backdrop-filter: blur(8px);\n    -webkit-backdrop-filter: blur(8px);\n    animation: fadeIn 0.3s ease;\n\n    .loading-text {\n      color: var(--theme-text-primary);\n      font-size: 14px;\n      text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n    }\n\n    .is-loading {\n      font-size: 24px;\n      color: var(--theme-primary);\n      animation: rotating 2s linear infinite;\n      filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));\n    }\n  }\n\n  // 空状态\n  &__empty {\n    @include flex-center;\n    flex-direction: column;\n    gap: 16px;\n    height: 100%;\n\n    .empty-text {\n      color: var(--theme-text-primary);\n      font-size: 14px;\n      margin: 8px 0 16px;\n    }\n\n    .el-button {\n      @include flex-center;\n      gap: 8px;\n      padding: 8px 20px;\n      height: 40px;\n      font-size: 14px;\n      transition: var(--theme-transition);\n      background: var(--theme-primary);\n      border-color: var(--theme-primary);\n\n      .el-icon {\n        font-size: 16px;\n      }\n\n      &:hover {\n        transform: translateY(-2px);\n        box-shadow: var(--theme-shadow-sm);\n      }\n    }\n  }\n\n  // 返回顶部按钮\n  &__backtop {\n    @include flex-center;\n    width: 40px;\n    height: 40px;\n    color: var(--theme-primary);\n    background: var(--theme-card-bg);\n    border-radius: var(--theme-radius);\n    box-shadow: var(--theme-shadow);\n    transition: var(--theme-transition);\n\n    &:hover {\n      background: var(--theme-primary);\n      color: #fff;\n      transform: translateY(-2px);\n    }\n\n    .el-icon {\n      font-size: 20px;\n    }\n  }\n}\n\n// 对话框样式\n.dialog-header {\n  h3 {\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    margin: 0;\n    font-weight: 600;\n    color: var(--theme-text-primary);\n\n    .title-main {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      font-size: 16px;\n    }\n\n    .title-sub {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      font-size: 14px;\n      color: var(--theme-text-secondary);\n      font-weight: normal;\n    }\n\n    .resource-title {\n      max-width: 300px;\n      @include text-ellipsis;\n    }\n  }\n\n  .file-size {\n    color: var(--theme-text-secondary);\n  }\n}\n\n.dialog-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 12px;\n  padding-top: 16px;\n}\n\n:deep(.el-dialog) {\n  border-radius: var(--theme-radius);\n  overflow: hidden;\n\n  .el-dialog__header {\n    margin: 0;\n    padding: 20px 24px;\n    border-bottom: 1px solid var(--el-border-color-lighter);\n  }\n\n  .el-dialog__body {\n    padding: 24px;\n  }\n\n  .el-dialog__footer {\n    padding: 16px 24px;\n    border-top: 1px solid var(--el-border-color-lighter);\n  }\n}\n\n// 表格扩展列样式\n:deep(.el-table) {\n  .el-table__expand-column {\n    .cell {\n      padding: 0;\n    }\n  }\n\n  .el-table__expanded-cell {\n    padding: 20px;\n  }\n\n  .el-table__expand-icon {\n    height: 23px;\n    line-height: 23px;\n  }\n}\n\n// 加载动画\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    backdrop-filter: blur(0);\n  }\n  to {\n    opacity: 1;\n    backdrop-filter: blur(8px);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/Setting.vue",
    "content": "<template>\n  <div class=\"settings-page\">\n    <!-- 项目配置卡片 -->\n    <el-card v-if=\"settingStore.globalSetting\" class=\"settings-card network-card\">\n      <template #header>\n        <div class=\"card-header\">\n          <el-icon><Connection /></el-icon>\n          <h2>项目配置</h2>\n        </div>\n      </template>\n\n      <div class=\"settings-section\">\n        <!-- 代理配置组 -->\n        <div class=\"settings-group\">\n          <div class=\"group-header\">\n            <h3>代理设置</h3>\n            <el-switch\n              v-model=\"localGlobalSetting.isProxyEnabled\"\n              active-text=\"已启用\"\n              @change=\"handleProxyChange\"\n            />\n          </div>\n\n          <div class=\"form-row\">\n            <div class=\"form-item\">\n              <label for=\"proxyDomain\">代理服务器IP</label>\n              <el-input\n                id=\"proxyDomain\"\n                v-model=\"localGlobalSetting.httpProxyHost\"\n                placeholder=\"127.0.0.1\"\n                :disabled=\"!localGlobalSetting.isProxyEnabled\"\n                @input=\"handleProxyHostChange\"\n              >\n                <template #prefix>\n                  <el-icon><Monitor /></el-icon>\n                </template>\n              </el-input>\n            </div>\n\n            <div class=\"form-item\">\n              <label for=\"proxyPort\">代理端口</label>\n              <el-input\n                id=\"proxyPort\"\n                v-model=\"localGlobalSetting.httpProxyPort\"\n                placeholder=\"7890\"\n                :disabled=\"!localGlobalSetting.isProxyEnabled\"\n              >\n                <template #prefix>\n                  <el-icon><Position /></el-icon>\n                </template>\n              </el-input>\n            </div>\n          </div>\n        </div>\n\n        <!-- 注册码配置组 -->\n        <div class=\"settings-group\">\n          <h3>注册码设置</h3>\n          <div class=\"form-row\">\n            <div class=\"form-item\">\n              <label for=\"AdminUserCode\">管理员注册码</label>\n              <el-input-number\n                id=\"AdminUserCode\"\n                v-model=\"localGlobalSetting.AdminUserCode\"\n                :controls=\"false\"\n                :precision=\"0\"\n                placeholder=\"设置管理员注册码\"\n              >\n                <template #prefix>\n                  <el-icon><Key /></el-icon>\n                </template>\n              </el-input-number>\n            </div>\n\n            <div class=\"form-item\">\n              <label for=\"CommonUserCode\">普通用户注册码</label>\n              <el-input-number\n                id=\"CommonUserCode\"\n                v-model=\"localGlobalSetting.CommonUserCode\"\n                :controls=\"false\"\n                :precision=\"0\"\n                placeholder=\"设置普通用户注册码\"\n              >\n                <template #prefix>\n                  <el-icon><Key /></el-icon>\n                </template>\n              </el-input-number>\n            </div>\n          </div>\n        </div>\n      </div>\n    </el-card>\n\n    <!-- 用户配置卡片 -->\n    <el-card class=\"settings-card user-card\">\n      <template #header>\n        <div class=\"card-header\">\n          <el-icon><User /></el-icon>\n          <h2>用户配置</h2>\n        </div>\n      </template>\n\n      <div class=\"settings-section\">\n        <div class=\"settings-group\">\n          <h3>网盘授权</h3>\n          <div class=\"form-row\">\n            <div class=\"form-item full-width\">\n              <label for=\"cookie115\">115网盘 Cookie</label>\n              <el-input\n                id=\"cookie115\"\n                v-model=\"localUserSettings.cloud115Cookie\"\n                type=\"password\"\n                show-password\n                placeholder=\"请输入115网盘Cookie\"\n              >\n                <template #prefix>\n                  <el-icon><Lock /></el-icon>\n                </template>\n              </el-input>\n            </div>\n          </div>\n\n          <div class=\"form-row\">\n            <div class=\"form-item full-width\">\n              <label for=\"cookieQuark\">夸克网盘 Cookie</label>\n              <el-input\n                id=\"cookieQuark\"\n                v-model=\"localUserSettings.quarkCookie\"\n                type=\"password\"\n                show-password\n                placeholder=\"请输入夸克网盘Cookie\"\n              >\n                <template #prefix>\n                  <el-icon><Lock /></el-icon>\n                </template>\n              </el-input>\n            </div>\n          </div>\n        </div>\n\n        <!-- 帮助链接 -->\n        <div class=\"settings-help\">\n          <h3>帮助文档</h3>\n          <div class=\"help-links\">\n            <el-link\n              href=\"https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l?singleDoc=true\"\n              target=\"_blank\"\n              type=\"primary\"\n            >\n              <el-icon><QuestionFilled /></el-icon>\n              CloudSaver部署与使用常见问题\n            </el-link>\n            <el-link\n              href=\"https://www.yuque.com/xiaoruihenbangde/ggogn3/cl2g0p9h3xrgfa5i\"\n              target=\"_blank\"\n              type=\"primary\"\n            >\n              <el-icon><QuestionFilled /></el-icon>\n              CloudSaver功能介绍\n            </el-link>\n            <el-link\n              href=\"https://alist.nn.ci/zh/guide/drivers/115.html#cookie获取方式\"\n              target=\"_blank\"\n              type=\"primary\"\n            >\n              <el-icon><QuestionFilled /></el-icon>\n              如何获取115网盘Cookie？\n            </el-link>\n            <el-link\n              href=\"https://alist.nn.ci/zh/guide/drivers/quark.html#cookie\"\n              target=\"_blank\"\n              type=\"primary\"\n            >\n              <el-icon><QuestionFilled /></el-icon>\n              如何获取夸克网盘Cookie？\n            </el-link>\n          </div>\n        </div>\n      </div>\n    </el-card>\n\n    <!-- 保存按钮 -->\n    <div class=\"settings-actions\">\n      <el-button type=\"primary\" @click=\"handleSave\"> 保存设置 </el-button>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useUserSettingStore } from \"@/stores/userSetting\";\nimport { ref, watch } from \"vue\";\nimport { ElMessage } from \"element-plus\";\nimport type { GlobalSettingAttributes, UserSettingAttributes } from \"@/types/user\";\nimport {\n  Connection,\n  Monitor,\n  Position,\n  Key,\n  User,\n  Lock,\n  QuestionFilled,\n} from \"@element-plus/icons-vue\";\n\nconst settingStore = useUserSettingStore();\n\n// 本地状态\nconst localGlobalSetting = ref<GlobalSettingAttributes>({\n  httpProxyHost: \"127.0.0.1\",\n  httpProxyPort: \"7890\",\n  isProxyEnabled: false,\n  AdminUserCode: 230713,\n  CommonUserCode: 9527,\n});\n\nconst localUserSettings = ref<UserSettingAttributes>({\n  cloud115Cookie: \"\",\n  quarkCookie: \"\",\n});\n\n// 监听 store 变化,更新本地状态\nwatch(\n  () => settingStore.globalSetting,\n  (newVal) => {\n    if (newVal) {\n      localGlobalSetting.value = { ...newVal };\n    }\n  },\n  { immediate: true }\n);\n\nwatch(\n  () => settingStore.userSettings,\n  (newVal) => {\n    if (newVal) {\n      localUserSettings.value = { ...newVal };\n    }\n  },\n  { immediate: true }\n);\n\n// 初始化获取设置\nsettingStore.getSettings();\n\n// 处理代理开关变化并立即保存\nconst handleProxyChange = async (val: boolean) => {\n  try {\n    localGlobalSetting.value.isProxyEnabled = val;\n    await settingStore.saveSettings({\n      globalSetting: localGlobalSetting.value,\n      userSettings: localUserSettings.value,\n    });\n    ElMessage.success(\"设置保存成功\");\n  } catch (error) {\n    // 保存失败时恢复开关状态\n    ElMessage.error(\"设置保存失败\");\n    localGlobalSetting.value.isProxyEnabled = !val;\n  }\n};\n\n// 处理代理地址,去除协议前缀\nconst handleProxyHostChange = (val: string) => {\n  // 移除 http:// 或 https:// 前缀\n  const cleanHost = val.replace(/^(https?:\\/\\/)/i, \"\");\n  // 更新状态\n  localGlobalSetting.value.httpProxyHost = cleanHost;\n};\n\n// 其他设置的保存\nconst handleSave = async () => {\n  try {\n    await settingStore.saveSettings({\n      globalSetting: localGlobalSetting.value,\n      userSettings: localUserSettings.value,\n    });\n    ElMessage.success(\"设置保存成功\");\n  } catch (error) {\n    console.error(\"保存设置失败:\", error);\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"@/styles/common.scss\";\n\n.settings-page {\n  // max-width: 1000px;\n  margin: 0;\n  padding-bottom: 40px;\n}\n\n.settings-card {\n  margin-bottom: 24px;\n  border-radius: var(--theme-radius);\n  transition: var(--theme-transition);\n  border: 1px solid rgba(0, 0, 0, 0.08);\n\n  &:hover {\n    box-shadow: var(--theme-shadow);\n  }\n\n  :deep(.el-card__header) {\n    padding: 16px 20px;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.06);\n  }\n}\n\n.card-header {\n  @include flex-center;\n  gap: 12px;\n\n  .el-icon {\n    font-size: 20px;\n    color: var(--theme-primary);\n  }\n\n  h2 {\n    margin: 0;\n    font-size: 16px;\n    font-weight: 600;\n    color: var(--theme-text-primary);\n  }\n}\n\n.settings-section {\n  padding: 20px;\n}\n\n.settings-group {\n  margin-bottom: 32px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n\n  h3 {\n    margin: 0 0 16px;\n    font-size: 14px;\n    font-weight: 600;\n    color: var(--theme-text-regular);\n  }\n\n  .group-header {\n    @include flex-center;\n    justify-content: space-between;\n    margin-bottom: 16px;\n\n    h3 {\n      margin: 0;\n    }\n  }\n}\n\n.form-row {\n  display: flex;\n  gap: 24px;\n  margin-bottom: 16px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n.form-item {\n  flex: 1;\n  min-width: 0;\n\n  &.full-width {\n    width: 100%;\n  }\n\n  label {\n    display: block;\n    margin-bottom: 8px;\n    font-size: 13px;\n    color: var(--theme-text-secondary);\n  }\n\n  :deep(.el-input),\n  :deep(.el-input-number) {\n    width: 100%;\n\n    .el-input__wrapper {\n      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);\n      transition: var(--theme-transition);\n\n      &:hover {\n        box-shadow: 0 0 0 1px var(--theme-primary);\n      }\n\n      &.is-focus {\n        box-shadow:\n          0 0 0 1px var(--theme-primary),\n          0 0 0 3px rgba(0, 102, 204, 0.1);\n      }\n    }\n\n    .el-input__prefix-inner {\n      .el-icon {\n        margin-right: 8px;\n        color: var(--theme-text-secondary);\n      }\n    }\n  }\n}\n\n.settings-help {\n  padding-top: 24px;\n  margin-top: 24px;\n  border-top: 1px solid rgba(0, 0, 0, 0.06);\n\n  .help-links {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 12px;\n    margin-top: 16px;\n  }\n\n  :deep(.el-link) {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    font-size: 14px;\n\n    .el-icon {\n      font-size: 16px;\n    }\n\n    &:hover {\n      transform: translateX(4px);\n    }\n  }\n}\n\n.settings-actions {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 24px;\n\n  .el-button {\n    min-width: 120px;\n    height: 40px;\n    border-radius: 20px;\n    font-size: 14px;\n    transition: var(--theme-transition);\n\n    .el-icon {\n      margin-right: 6px;\n      font-size: 16px;\n    }\n\n    &:hover {\n      transform: translateY(-2px);\n      box-shadow: var(--theme-shadow-sm);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/Thanks.vue",
    "content": "<template>\n  <div ref=\"containerRef\" class=\"thanks-container\">\n    <div ref=\"titleRef\" class=\"title\">感谢Ta们对项目的赞赏</div>\n\n    <!-- 添加说明文字 -->\n    <div class=\"description\">\n      <p>感谢每一位支持者的信任与鼓励</p>\n      <p>正是你们的支持让这个项目能够持续发展</p>\n    </div>\n\n    <div ref=\"sponsorsContainer\" class=\"sponsors-container\">\n      <div\n        v-for=\"(sponsor, index) in randomizedSponsors\"\n        :key=\"sponsor.name\"\n        ref=\"avatarRefs\"\n        class=\"sponsor-avatar\"\n        @mouseenter=\"handleMouseEnter(index)\"\n        @mouseleave=\"handleMouseLeave\"\n      >\n        <div\n          ref=\"avatarWrapperRefs\"\n          class=\"avatar-wrapper\"\n          :class=\"{\n            active: activeIndex === index,\n            'has-link': sponsor.link,\n          }\"\n          @click=\"handleAvatarClick(sponsor.link)\"\n        >\n          <div class=\"avatar-inner\">\n            <div class=\"avatar-overlay\"></div>\n            <img :src=\"sponsor.avatar\" :alt=\"sponsor.name\" class=\"avatar-img\" />\n            <div class=\"name-tag\">\n              {{ sponsor.name }}\n            </div>\n          </div>\n        </div>\n\n        <div v-if=\"activeIndex === index && sponsor.message\" class=\"dialog-box\">\n          <div class=\"dialog-content\">\n            <div :id=\"`typeIt-${index}`\" class=\"type-it-container\"></div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- 添加赞赏按钮 -->\n    <a\n      :href=\"PROJECT_GITHUB + '?tab=readme-ov-file#支持项目'\"\n      target=\"_blank\"\n      class=\"sponsor-button\"\n      @mouseenter=\"handleButtonHover\"\n      @mouseleave=\"handleButtonLeave\"\n    >\n      <div class=\"button-content\">\n        <svg class=\"heart-icon\" viewBox=\"0 0 24 24\">\n          <path\n            d=\"M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z\"\n          />\n        </svg>\n        <span>赞赏支持</span>\n      </div>\n    </a>\n  </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, nextTick, computed, onBeforeUnmount } from \"vue\";\nimport TypeIt from \"typeit\";\nimport { userApi } from \"@/api/user\";\nimport gsap from \"gsap\";\nimport { PROJECT_GITHUB } from \"@/constants/project\";\n// 赞助者数据\nconst sponsors = ref([]);\n\nconst getSponsors = async () => {\n  const res = await userApi.getSponsors();\n  sponsors.value = res.data;\n};\n\n// 随机排序赞助者\nconst randomizedSponsors = computed(() => {\n  // 有sort的按照sort排序并排在前面，没有的按照随机排序\n  const sortedSponsors = [...sponsors.value]\n    .filter((item) => item.sort)\n    .sort((a, b) => a.sort - b.sort);\n  const randomSponsors = [...sponsors.value]\n    .filter((item) => !item.sort)\n    .sort(() => Math.random() - 0.5);\n  return [...sortedSponsors, ...randomSponsors];\n});\n\nconst containerRef = ref(null);\nconst sponsorsContainer = ref(null);\nconst activeIndex = ref(null);\nconst avatarRefs = ref([]);\nconst avatarWrapperRefs = ref([]);\nlet typeItInstance = null;\nconst activeCenter = ref({ x: 0, y: 0 });\nconst titleRef = ref(null);\n\n// 添加头像动画时间轴的引用\nconst avatarTimelines = ref([]);\n\n// 使用 requestAnimationFrame 优化动画更新\nlet rafId = null;\n\n// 添加一个变量来跟踪当前激活的头像\nlet currentHoverIndex = null;\n\nonMounted(async () => {\n  await getSponsors();\n\n  // 修改页面入场动画\n  const tl = gsap.timeline({\n    defaults: { ease: \"power3.out\" },\n  });\n\n  // 同时执行所有元素的动画\n  tl.from([titleRef.value, sponsorsContainer.value, ...avatarWrapperRefs.value], {\n    y: -20,\n    opacity: 0,\n    duration: 0.6,\n    stagger: {\n      amount: 0.3,\n      from: \"start\",\n    },\n    ease: \"back.out(1.2)\",\n  });\n\n  // 添加可见性变化监听\n  document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n\n  // 添加窗口失焦事件处理\n  window.addEventListener(\"blur\", handleMouseLeave);\n});\n\n// 修改 handleVisibilityChange 函数\nconst handleVisibilityChange = () => {\n  if (document.hidden) {\n    // 页面不可见时清理资源\n    if (typeItInstance) {\n      typeItInstance.destroy();\n      typeItInstance = null;\n    }\n  }\n};\n\n// 修改鼠标移入处理函数\nconst handleMouseEnter = (() => {\n  let timeout;\n  return async (index) => {\n    if (timeout) {\n      clearTimeout(timeout);\n    }\n    currentHoverIndex = index;\n    activeIndex.value = index;\n\n    timeout = setTimeout(async () => {\n      // 确保这是最新的hover状态\n      if (currentHoverIndex !== index) return;\n\n      const activeAvatar = avatarWrapperRefs.value[index];\n      if (activeAvatar) {\n        const rect = activeAvatar.getBoundingClientRect();\n        activeCenter.value = {\n          x: rect.left + rect.width / 2,\n          y: rect.top + rect.height / 2,\n        };\n      }\n\n      // 暂停所有浮动动画\n      avatarTimelines.value.forEach((timeline) => {\n        if (timeline) {\n          timeline.pause();\n        }\n      });\n\n      updateAvatarsEffect(index);\n\n      await nextTick();\n\n      try {\n        // 初始化打字效果\n        if (typeItInstance) {\n          typeItInstance.destroy();\n          typeItInstance = null;\n        }\n\n        const typeItElement = document.getElementById(`typeIt-${index}`);\n        if (typeItElement) {\n          typeItInstance = new TypeIt(typeItElement, {\n            strings: randomizedSponsors.value[index].message,\n            speed: 20,\n            waitUntilVisible: true,\n          }).go();\n        }\n      } catch (error) {\n        console.error(\"TypeIt初始化错误:\", error);\n      }\n    }, 16);\n  };\n})();\n\n// 更新所有头像效果\nconst updateAvatarsEffect = (activeIndex) => {\n  if (!avatarWrapperRefs.value || activeCenter.value.x === 0) return;\n\n  if (rafId) {\n    cancelAnimationFrame(rafId);\n  }\n\n  rafId = requestAnimationFrame(() => {\n    avatarWrapperRefs.value.forEach((wrapper, index) => {\n      const inner = wrapper.querySelector(\".avatar-inner\");\n      const avatarContainer = wrapper.closest(\".sponsor-avatar\");\n\n      if (index === activeIndex) {\n        gsap.to(inner, {\n          scale: 1.2,\n          y: -15,\n          zIndex: 10,\n          duration: 0.2,\n          ease: \"back.out(1.5)\",\n          force3D: true,\n        });\n\n        gsap.to(avatarContainer, {\n          filter: \"drop-shadow(0 20px 30px rgba(0, 0, 0, 0.25))\",\n          duration: 0.2,\n        });\n\n        const activeOverlay = wrapper.querySelector(\".avatar-overlay\");\n        gsap.to(activeOverlay, {\n          opacity: 0,\n          duration: 0.15,\n        });\n        return;\n      }\n\n      const rect = wrapper.getBoundingClientRect();\n      const centerX = rect.left + rect.width / 2;\n      const centerY = rect.top + rect.height / 2;\n      const deltaX = activeCenter.value.x - centerX;\n      const deltaY = activeCenter.value.y - centerY;\n      const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);\n\n      if (distance < 0.1) return;\n\n      const maxDistance = 400;\n      const strength = Math.max(0, 1 - distance / maxDistance);\n\n      // 计算吸引力效果\n      const attractionStrength = Math.pow(strength, 1.5);\n      const moveX = (deltaX / distance) * 30 * attractionStrength;\n      const moveY = (deltaY / distance) * 30 * attractionStrength;\n\n      // 计算旋转角度\n      const rotateX = -Math.atan2(deltaY, distance) * (180 / Math.PI) * strength;\n      const rotateY = Math.atan2(deltaX, distance) * (180 / Math.PI) * strength;\n\n      // 应用变换效果\n      gsap.to(inner, {\n        scale: 1 + 0.05 * strength,\n        x: moveX,\n        y: moveY,\n        rotationX: rotateX,\n        rotationY: rotateY,\n        duration: 0.2,\n        ease: \"power2.out\",\n        force3D: true,\n      });\n\n      // 更新阴影效果\n      const shadowOffsetX = (deltaX / distance) * 15 * strength;\n      const shadowOffsetY = Math.max(6, (deltaY / distance) * 20 * strength + 6);\n      const shadowBlur = 12 + 18 * strength;\n      const shadowOpacity = 0.15 + 0.1 * strength;\n\n      gsap.to(avatarContainer, {\n        filter: `drop-shadow(${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px rgba(0, 0, 0, ${shadowOpacity}))`,\n        duration: 0.2,\n      });\n    });\n  });\n};\n\n// 修改鼠标移出处理函数\nconst handleMouseLeave = () => {\n  currentHoverIndex = null;\n  activeIndex.value = null;\n  activeCenter.value = { x: 0, y: 0 };\n\n  if (!avatarWrapperRefs.value) return;\n\n  avatarWrapperRefs.value.forEach((wrapper) => {\n    const inner = wrapper.querySelector(\".avatar-inner\");\n    if (inner) {\n      gsap.killTweensOf(inner);\n\n      gsap.to(inner, {\n        scale: 1,\n        y: 0,\n        x: 0,\n        rotation: 0,\n        rotationX: 0,\n        rotationY: 0,\n        duration: 0.2,\n        ease: \"power2.out\",\n      });\n    }\n\n    const avatarContainer = wrapper.closest(\".sponsor-avatar\");\n    if (avatarContainer) {\n      gsap.killTweensOf(avatarContainer);\n\n      gsap.to(avatarContainer, {\n        filter: \"drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15))\",\n        duration: 0.2,\n      });\n    }\n\n    const overlayElement = wrapper.querySelector(\".avatar-overlay\");\n    if (overlayElement) {\n      gsap.to(overlayElement, {\n        opacity: 1,\n        duration: 0.15,\n      });\n    }\n  });\n\n  if (typeItInstance) {\n    typeItInstance.destroy();\n    typeItInstance = null;\n  }\n};\n\n// 添加点击处理函数\nconst handleAvatarClick = (link) => {\n  if (link) {\n    window.open(link, \"_blank\");\n  }\n};\n\n// 组件卸载时清理\nonBeforeUnmount(() => {\n  window.removeEventListener(\"blur\", handleMouseLeave);\n  document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n\n  // 清理打字实例\n  if (typeItInstance) {\n    typeItInstance.destroy();\n    typeItInstance = null;\n  }\n});\n\n// 添加按钮悬浮效果\nconst handleButtonHover = () => {\n  gsap.to(\".sponsor-button\", {\n    scale: 1.05,\n    duration: 0.3,\n    ease: \"power2.out\",\n  });\n};\n\nconst handleButtonLeave = () => {\n  gsap.to(\".sponsor-button\", {\n    scale: 1,\n    duration: 0.3,\n    ease: \"power2.out\",\n  });\n};\n</script>\n\n<style scoped>\n.thanks-container {\n  width: 100%;\n  box-sizing: border-box;\n  height: calc(100vh - 100px);\n  overflow: auto;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding: 40px 20px;\n  background: linear-gradient(135deg, #f6f8fd 0%, #f1f4f9 100%);\n  position: relative;\n  z-index: 1;\n  transform: translateZ(0);\n  will-change: transform;\n  backface-visibility: hidden;\n}\n\n.gradient-circle {\n  position: absolute;\n  border-radius: 50%;\n  filter: blur(40px);\n  opacity: 0.5;\n  will-change: transform;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n\n.circle-1 {\n  width: 600px;\n  height: 600px;\n  background: linear-gradient(45deg, rgba(142, 68, 173, 0.2), rgba(91, 177, 235, 0.2));\n  top: -200px;\n  left: -200px;\n}\n\n.circle-2 {\n  width: 500px;\n  height: 500px;\n  background: linear-gradient(45deg, rgba(91, 177, 235, 0.2), rgba(142, 68, 173, 0.2));\n  bottom: -150px;\n  right: -150px;\n}\n\n.circle-3 {\n  width: 400px;\n  height: 400px;\n  background: linear-gradient(45deg, rgba(241, 196, 15, 0.1), rgba(142, 68, 173, 0.1));\n  top: 40%;\n  left: 30%;\n}\n\n/* 装饰层 */\n.decoration-layer {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  z-index: 0;\n}\n\n.floating-dot {\n  position: absolute;\n  width: 6px;\n  height: 6px;\n  background: rgba(142, 68, 173, 0.2);\n  border-radius: 50%;\n  animation: floatingDot 8s ease-in-out infinite;\n  animation-delay: var(--delay);\n  will-change: transform;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n\n@keyframes floatingDot {\n  0%,\n  100% {\n    transform: translate(0, 0);\n  }\n  25% {\n    transform: translate(100px, 50px);\n  }\n  50% {\n    transform: translate(50px, 100px);\n  }\n  75% {\n    transform: translate(-50px, 50px);\n  }\n}\n\n.title {\n  margin-bottom: 20px;\n  font-size: 50px;\n  color: #2c3e50;\n  text-align: center;\n  font-weight: 700;\n  background: linear-gradient(45deg, #8e44ad, #3498db);\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);\n  letter-spacing: 1px;\n  will-change: transform, opacity;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n\n.sponsors-container {\n  width: 70%;\n  max-width: 1200px;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));\n  grid-gap: 40px;\n  justify-content: center;\n  padding: 20px;\n  will-change: transform, opacity;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n  opacity: 1; /* 确保容器默认可见 */\n}\n\n.sponsor-avatar {\n  position: relative;\n  transform-style: preserve-3d;\n  transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);\n  z-index: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding-bottom: 20px;\n  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15));\n  transition: all 0.3s ease;\n}\n\n.avatar-wrapper {\n  width: 80px;\n  height: 80px;\n  position: relative;\n  z-index: 1;\n}\n\n.avatar-inner {\n  width: 100% !important;\n  height: 100% !important;\n  border-radius: 50%;\n  border: 4px solid #ffffff;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n  transition:\n    transform 0.2s ease,\n    filter 0.2s ease;\n  cursor: pointer;\n  position: relative;\n  isolation: isolate;\n  transform-style: preserve-3d;\n  box-sizing: border-box;\n}\n\n.avatar-overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  border-radius: 50%;\n  overflow: hidden;\n  background: linear-gradient(\n    135deg,\n    rgba(255, 255, 255, 0.2) 0%,\n    rgba(255, 255, 255, 0) 50%,\n    rgba(0, 0, 0, 0.1) 100%\n  );\n  opacity: 1;\n  transition: all 0.3s ease;\n  pointer-events: none;\n  z-index: 3;\n  mix-blend-mode: overlay; /* 添加混合模式增强效果 */\n}\n\n.avatar-wrapper.active .avatar-inner {\n  transform: scale(1.2) translateY(-10px);\n}\n\n.avatar-wrapper.has-link {\n  position: relative;\n  cursor: pointer;\n}\n\n.avatar-wrapper.has-link::before {\n  content: \"\";\n  position: absolute;\n  inset: -4px;\n  border-radius: 50%;\n  background: linear-gradient(45deg, #ff3366, #ff6b6b, #4ecdc4, #45b7d1, #96e6a1);\n  opacity: 0;\n  transition: opacity 0.3s ease;\n  z-index: -1;\n  filter: blur(8px);\n}\n\n.avatar-wrapper.has-link:hover::before {\n  opacity: 0.8;\n  animation: borderGlow 2s linear infinite;\n}\n\n.glow-effect {\n  position: absolute;\n  inset: 0;\n  border-radius: 50%;\n  background: transparent;\n  border: 2px solid transparent;\n  transition: all 0.3s ease;\n  z-index: 2;\n}\n\n.avatar-wrapper.has-link:hover .glow-effect {\n  border-color: rgba(255, 255, 255, 0.5);\n  box-shadow:\n    0 0 20px rgba(255, 255, 255, 0.3),\n    inset 0 0 20px rgba(255, 255, 255, 0.3);\n}\n\n@keyframes borderGlow {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n/* 确保激活状态下的发光效果仍然可见 */\n.avatar-wrapper.active.has-link::before {\n  z-index: -1;\n}\n\n.avatar-img {\n  width: 100%;\n  height: 100%;\n  border-radius: 50%;\n  overflow: hidden;\n  object-fit: cover;\n  position: relative;\n  z-index: 2;\n}\n\n.dialog-box {\n  position: absolute;\n  top: -120px; /* 稍微上调对话框位置 */\n  left: 50%;\n  transform: translateX(-50%);\n  background: rgba(255, 255, 255, 0.95);\n  backdrop-filter: blur(8px);\n  padding: 16px 20px;\n  border-radius: 16px;\n  box-shadow:\n    0 4px 24px -1px rgba(0, 0, 0, 0.1),\n    0 2px 8px -1px rgba(0, 0, 0, 0.06),\n    inset 0 0 0 1px rgba(255, 255, 255, 0.5),\n    0 0 40px rgba(142, 68, 173, 0.05);\n  min-width: 180px;\n  z-index: 111;\n  opacity: 0;\n  animation: dialogFadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n  border: 1px solid rgba(0, 0, 0, 0.05);\n  will-change: transform, opacity;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n\n.dialog-content {\n  position: relative;\n  font-size: 15px;\n  line-height: 1.6;\n  color: #2c3e50;\n  margin: 0;\n  text-align: center;\n  text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);\n}\n\n/* 修改引号装饰的样式 */\n.dialog-content::before,\n.dialog-content::after {\n  content: '\"';\n  position: absolute;\n  font-size: 28px;\n  color: #8e44ad;\n  opacity: 0.15;\n  text-shadow: none;\n}\n\n.dialog-content::before {\n  left: -15px;\n  top: -12px;\n}\n\n.dialog-content::after {\n  right: -15px;\n  bottom: -24px;\n}\n\n/* 优化淡入动画，使其更加流畅 */\n@keyframes dialogFadeIn {\n  0% {\n    opacity: 0;\n    transform: translateX(-50%) translateY(10px) scale(0.98);\n    filter: blur(1px);\n  }\n  100% {\n    opacity: 1;\n    transform: translateX(-50%) translateY(0) scale(1);\n    filter: blur(0);\n  }\n}\n\n/* 优化打字效果容器样式 */\n.type-it-container {\n  min-height: 24px;\n  padding: 4px 8px;\n  position: relative;\n  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));\n  border-radius: 8px;\n}\n\n/* 添加打字光标样式 */\n.ti-cursor {\n  color: #8e44ad;\n  font-weight: 300;\n}\n\n.name-tag {\n  position: absolute;\n  bottom: -10px;\n  left: 50%;\n  transform: translateX(-50%);\n  text-align: center;\n  color: #2c3e50;\n  opacity: 1;\n  font-weight: 500;\n  font-size: 12px;\n  z-index: 20;\n  background-color: #fff;\n  border-radius: 15px;\n  padding: 0px 10px;\n  white-space: nowrap; /* 防止文字换行 */\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n}\n\n/* 添加新的样式 */\n.description {\n  text-align: center;\n  margin-bottom: 40px;\n  color: #666;\n  line-height: 1.6;\n  max-width: 600px;\n  margin-inline: auto;\n}\n\n.description p {\n  margin: 8px 0;\n  font-size: 16px;\n}\n\n.bottom-text {\n  text-align: center;\n  margin-top: 60px;\n  color: #666;\n  line-height: 1.6;\n}\n\n.bottom-text p {\n  margin: 8px 0;\n  font-size: 16px;\n}\n\n.sponsor-button {\n  position: fixed;\n  bottom: 40px;\n  right: 40px;\n  background: linear-gradient(45deg, #ff3366, #ff6b6b);\n  color: white;\n  padding: 12px 24px;\n  border-radius: 30px;\n  text-decoration: none;\n  font-weight: 500;\n  font-size: 16px;\n  box-shadow:\n    0 4px 15px rgba(255, 51, 102, 0.3),\n    0 2px 8px rgba(255, 51, 102, 0.2);\n  transition: all 0.3s ease;\n  z-index: 1000;\n}\n\n.sponsor-button:hover {\n  transform: translateY(-2px);\n  box-shadow:\n    0 6px 20px rgba(255, 51, 102, 0.4),\n    0 3px 10px rgba(255, 51, 102, 0.3);\n}\n\n.button-content {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.heart-icon {\n  width: 20px;\n  height: 20px;\n  fill: currentColor;\n  animation: heartBeat 1.2s ease-in-out infinite;\n}\n\n@keyframes heartBeat {\n  0% {\n    transform: scale(1);\n  }\n  14% {\n    transform: scale(1.3);\n  }\n  28% {\n    transform: scale(1);\n  }\n  42% {\n    transform: scale(1.3);\n  }\n  70% {\n    transform: scale(1);\n  }\n}\n\n/* 添加响应式样式 */\n@media (max-width: 768px) {\n  .sponsor-button {\n    bottom: 20px;\n    right: 20px;\n    padding: 10px 20px;\n    font-size: 14px;\n  }\n\n  .description,\n  .bottom-text {\n    padding: 0 20px;\n  }\n}\n\n/* 添加悬浮状态的阴影效果 */\n.sponsor-avatar:hover {\n  filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.15));\n}\n\n/* 修改激活状态的阴影效果 */\n.sponsor-avatar:has(.avatar-wrapper.active) {\n  filter: drop-shadow(0 15px 25px rgba(0, 0, 0, 0.2));\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/mobile/Douban.vue",
    "content": "<template>\n  <div class=\"mobile-page douban\">\n    <!-- 加载状态 -->\n    <div v-if=\"doubanStore.loading\" class=\"douban__loading\">\n      <van-loading type=\"spinner\" size=\"24px\" vertical>加载中...</van-loading>\n    </div>\n\n    <!-- 电影列表 -->\n    <div v-else class=\"douban__grid\">\n      <div v-for=\"movie in doubanStore.hotList\" :key=\"movie.id\" class=\"douban__item\">\n        <!-- 海报卡片 -->\n        <div class=\"douban__poster\">\n          <van-image\n            class=\"poster__img\"\n            :src=\"movie.cover\"\n            fit=\"cover\"\n            lazy\n            loading=\"skeleton\"\n            :alt=\"movie.title\"\n            @click=\"previewImage(movie.cover)\"\n          />\n          <!-- 评分标签 -->\n          <van-tag\n            class=\"poster__rate\"\n            type=\"primary\"\n            :style=\"{ backgroundColor: getRateColor(movie.rate) }\"\n          >\n            {{ movie.rate }}\n          </van-tag>\n          <!-- 搜索按钮 -->\n          <div class=\"poster__action\" @click.stop=\"searchMovie(movie.title)\">\n            <van-icon name=\"search\" size=\"24\" />\n          </div>\n        </div>\n        <!-- 电影信息 -->\n        <div class=\"douban__info\">\n          <van-button class=\"info__title\" type=\"default\" :url=\"movie.url\" text=\"text\" block>\n            {{ movie.title }}\n          </van-button>\n        </div>\n      </div>\n    </div>\n\n    <!-- 空状态 -->\n    <van-empty v-if=\"!doubanStore.loading && !doubanStore.hotList.length\" description=\"暂无数据\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, watch } from \"vue\";\nimport { useRouter, useRoute } from \"vue-router\";\nimport { useDoubanStore } from \"@/stores/douban\";\nimport { showImagePreview } from \"vant\";\n\ninterface CurrentParams {\n  type: string;\n  tag?: string;\n}\n\n// 路由相关\nconst router = useRouter();\nconst route = useRoute();\nconst routeParams = computed((): CurrentParams => ({ ...route.query }) as unknown as CurrentParams);\n\n// 状态管理\nconst doubanStore = useDoubanStore();\n\n// 监听路由参数变化\nwatch(\n  () => routeParams.value,\n  (params) => {\n    if (params) {\n      doubanStore.setCurrentParams(params);\n    }\n  },\n  { immediate: true }\n);\n\n// 方法定义\nconst searchMovie = (title: string) => {\n  router.push({\n    path: \"/resource\",\n    query: { keyword: title },\n  });\n};\n\nconst previewImage = (url: string) => {\n  showImagePreview({\n    images: [url],\n    closeable: true,\n  });\n};\n\n// 根据评分获取颜色\nconst getRateColor = (rate: string | number) => {\n  const numRate = Number(rate);\n  if (numRate >= 8) return \"#42b883\";\n  if (numRate >= 6) return \"#5853fa\";\n  return \"#f56c6c\";\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.douban {\n  // 网格布局 - 修改为两列\n  &__grid {\n    display: grid;\n    grid-template-columns: repeat(2, 1fr);\n    gap: var(--spacing-base);\n    padding: var(--spacing-base);\n  }\n\n  // 电影项\n  &__item {\n    background: var(--theme-other_background);\n    border-radius: var(--border-radius-lg);\n    overflow: hidden;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n  }\n\n  // 海报区域\n  &__poster {\n    position: relative;\n    aspect-ratio: 2/3;\n    background: #f5f5f5;\n    overflow: hidden;\n\n    .poster__img {\n      width: 100%;\n      height: 100%;\n      transition: transform 0.3s ease;\n\n      &:active {\n        transform: scale(1.05);\n      }\n    }\n\n    .poster__rate {\n      position: absolute;\n      top: var(--spacing-xs);\n      right: var(--spacing-xs);\n      font-size: 13px;\n      font-weight: 500;\n      padding: 2px 6px;\n      border-radius: var(--border-radius-lg);\n    }\n\n    .poster__action {\n      position: absolute;\n      inset: 0;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      background: rgba(0, 0, 0, 0.6);\n      opacity: 0;\n      transition: opacity 0.2s ease;\n      color: #fff;\n\n      &:active {\n        opacity: 1;\n      }\n    }\n  }\n\n  // 信息区域\n  &__info {\n    padding: 6px 8px;\n\n    .info__title {\n      font-size: 14px;\n      font-weight: 500;\n      line-height: 1.4;\n      color: var(--theme-color);\n      text-align: left;\n      border: none;\n\n      &:active {\n        color: var(--theme-theme);\n      }\n    }\n  }\n\n  // 加载状态\n  &__loading {\n    position: fixed;\n    inset: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: var(--theme-background);\n    z-index: 1;\n\n    :deep(.van-loading) {\n      padding: 16px 24px;\n      background: rgba(0, 0, 0, 0.7);\n      border-radius: 8px;\n      color: #fff;\n    }\n  }\n}\n\n// 深度修改 Vant 组件样式\n:deep(.van-image) {\n  display: block;\n  background: #f5f5f5;\n}\n\n:deep(.van-tag--primary) {\n  border: none;\n}\n\n:deep(.van-button) {\n  height: auto;\n  padding: var(--spacing-xs) 0;\n  border: none;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/mobile/Home.vue",
    "content": "<template>\n  <div class=\"home\">\n    <!-- 顶部搜索栏 -->\n    <header class=\"home__header\">\n      <div class=\"header__wrapper\">\n        <van-search\n          v-model=\"searchForm.keyword\"\n          class=\"header__search\"\n          shape=\"round\"\n          placeholder=\"请输入搜索关键词或输入链接直接解析\"\n          @search=\"handleSearch\"\n        />\n\n        <van-icon\n          name=\"https://b.yzcdn.cn/vant/icon-demo-1126.png\"\n          class=\"header__action\"\n          @click=\"handleLogout\"\n        />\n      </div>\n    </header>\n\n    <!-- 主要内容区 -->\n    <main class=\"home__content\">\n      <router-view v-slot=\"{ Component }\">\n        <transition name=\"fade\" mode=\"out-in\">\n          <component :is=\"Component\" />\n        </transition>\n      </router-view>\n    </main>\n\n    <!-- 底部导航栏 -->\n    <van-tabbar class=\"home__tabbar\" route>\n      <van-tabbar-item to=\"/resource\" icon=\"search\">搜索</van-tabbar-item>\n      <van-tabbar-item to=\"/douban\" icon=\"video\">热门</van-tabbar-item>\n      <van-tabbar-item to=\"/setting\" icon=\"setting-o\">设置</van-tabbar-item>\n    </van-tabbar>\n\n    <!-- 全局加载状态 -->\n    <van-overlay :show=\"resourceStore.loading\" class=\"home__loading\" @touchmove.prevent>\n      <van-loading type=\"spinner\" color=\"#fff\" size=\"24px\"> 资源搜索中... </van-loading>\n    </van-overlay>\n\n    <!-- 返回顶部 -->\n    <van-back-top right=\"30px\" bottom=\"100px\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watch } from \"vue\";\nimport { useRouter, useRoute } from \"vue-router\";\nimport { showConfirmDialog } from \"vant\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { useUserSettingStore } from \"@/stores/userSetting\";\n\n// 接口定义\ninterface SearchForm {\n  keyword: string;\n}\n\n// 状态管理\nconst resourceStore = useResourceStore();\nconst settingStore = useUserSettingStore();\n\n// 响应式数据\nconst searchForm = ref<SearchForm>({\n  keyword: \"\",\n});\n\n// 路由相关\nconst router = useRouter();\nconst route = useRoute();\n\n// 初始化\nsettingStore.getSettings();\n\n// 监听路由参数\nwatch(\n  () => route.query.keyword as string,\n  (keyword) => {\n    if (keyword) {\n      searchForm.value.keyword = keyword;\n      handleSearch();\n    } else {\n      searchForm.value.keyword = resourceStore.keyword;\n    }\n  }\n);\nwatch(\n  () => resourceStore.keyword,\n  (newKeyword) => {\n    searchForm.value.keyword = newKeyword;\n  }\n);\n\n// 方法定义\nconst handleSearch = async () => {\n  const keyword = searchForm.value.keyword.trim();\n  if (!keyword) return;\n\n  if (keyword.startsWith(\"http\")) {\n    await resourceStore.parsingCloudLink(keyword);\n    return;\n  }\n\n  if (route.path !== \"/resource\") {\n    await router.push(\"/resource\");\n  }\n  await resourceStore.searchResources(keyword);\n};\n\nconst handleLogout = () => {\n  showConfirmDialog({\n    title: \"退出登录\",\n    message: \"确定要退出登录吗？\",\n  }).then(() => {\n    localStorage.removeItem(\"token\");\n    router.push(\"/login\");\n  });\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.home {\n  // 布局\n  min-height: 100vh;\n  background: var(--theme-background);\n  display: flex;\n  flex-direction: column;\n\n  // 头部搜索\n  &__header {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 100;\n    background: var(--theme-other_background);\n    backdrop-filter: blur(8px);\n    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.05);\n\n    .header__wrapper {\n      display: flex;\n      align-items: center;\n      padding: 8px;\n    }\n\n    .header__search {\n      flex: 1;\n      padding: 0;\n      background: transparent;\n    }\n\n    .header__action {\n      padding: 8px;\n      margin-left: 4px;\n      color: var(--theme-color);\n      font-size: 24px;\n      cursor: pointer;\n      line-height: 1;\n\n      &:active {\n        color: var(--theme-theme);\n      }\n    }\n  }\n\n  // 主内容区 - 调整顶部间距\n  &__content {\n    padding-top: 64px; // 搜索框高度(48px) + 上下padding(8px * 2)\n    padding-bottom: 100px; // tabbar高度 + 底部安全区域\n    box-sizing: border-box;\n    flex: 1;\n  }\n\n  // 加载状态\n  &__loading {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 14px;\n    color: #fff;\n  }\n}\n\n// 过渡动画\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n// 深度修改 Vant 组件样式\n:deep(.van-tabbar) {\n  background: var(--theme-other_background);\n  backdrop-filter: blur(8px);\n  border-top: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n:deep(.van-tabbar-item) {\n  color: var(--theme-color);\n}\n\n:deep(.van-tabbar-item--active) {\n  color: var(--theme-theme);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/mobile/Login.vue",
    "content": "<template>\n  <div class=\"login\">\n    <!-- 背景区域 -->\n    <div class=\"login__background\" aria-hidden=\"true\" />\n\n    <!-- 主要内容区 -->\n    <main class=\"login__content\">\n      <!-- 头部 Logo -->\n      <header class=\"login__header\">\n        <img :src=\"logo\" alt=\"Cloud Saver Logo\" class=\"login__logo\" width=\"60\" height=\"60\" />\n        <h1 class=\"login__title\">Cloud Saver</h1>\n      </header>\n\n      <!-- 添加 Tab 切换 -->\n      <van-tabs v-model:active=\"activeTab\" class=\"login__tabs\">\n        <!-- 登录面板 -->\n        <van-tab title=\"登录\" name=\"login\">\n          <van-form class=\"login__form\" @submit=\"handleLogin\">\n            <van-cell-group inset class=\"login__form-group\">\n              <!-- 用户名输入框 -->\n              <van-field\n                v-model=\"loginForm.username\"\n                name=\"username\"\n                label=\"用户名\"\n                placeholder=\"请输入用户名\"\n                :rules=\"[{ required: true, message: '请填写用户名' }]\"\n                autocomplete=\"username\"\n                @keyup.enter=\"focusLoginPassword\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"user-o\" />\n                </template>\n              </van-field>\n\n              <!-- 密码输入框 -->\n              <van-field\n                ref=\"loginPasswordRef\"\n                v-model=\"loginForm.password\"\n                type=\"password\"\n                name=\"password\"\n                label=\"密码\"\n                placeholder=\"请输入密码\"\n                :rules=\"[{ required: true, message: '请填写密码' }]\"\n                autocomplete=\"current-password\"\n                @keyup.enter=\"handleLogin\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"lock\" />\n                </template>\n              </van-field>\n\n              <!-- 优化记住密码选项 -->\n              <div class=\"login__remember\">\n                <van-checkbox v-model=\"rememberPassword\" class=\"remember-checkbox\">\n                  记住密码\n                </van-checkbox>\n              </div>\n            </van-cell-group>\n\n            <!-- 登录按钮 -->\n            <div class=\"login__submit\">\n              <van-button\n                :loading=\"isLoading\"\n                :disabled=\"isLoading\"\n                round\n                block\n                type=\"primary\"\n                native-type=\"submit\"\n                class=\"login__button\"\n              >\n                {{ isLoading ? \"登录中...\" : \"登录\" }}\n              </van-button>\n            </div>\n          </van-form>\n        </van-tab>\n\n        <!-- 注册面板 -->\n        <van-tab title=\"注册\" name=\"register\">\n          <van-form class=\"login__form\" @submit=\"handleRegister\">\n            <van-cell-group inset class=\"login__form-group\">\n              <van-field\n                v-model=\"registerForm.username\"\n                name=\"username\"\n                label=\"用户名\"\n                placeholder=\"请输入用户名\"\n                :rules=\"usernameRules\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"user-o\" />\n                </template>\n              </van-field>\n\n              <van-field\n                v-model=\"registerForm.password\"\n                type=\"password\"\n                name=\"password\"\n                label=\"密码\"\n                placeholder=\"请输入密码\"\n                :rules=\"passwordRules\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"lock\" />\n                </template>\n              </van-field>\n\n              <van-field\n                v-model=\"registerForm.confirmPassword\"\n                type=\"password\"\n                name=\"confirmPassword\"\n                label=\"确认密码\"\n                placeholder=\"请确认密码\"\n                :rules=\"confirmPasswordRules\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"lock\" />\n                </template>\n              </van-field>\n\n              <van-field\n                v-model=\"registerForm.registerCode\"\n                name=\"registerCode\"\n                label=\"注册码\"\n                placeholder=\"请输入注册码\"\n                :rules=\"registerCodeRules\"\n              >\n                <template #left-icon>\n                  <van-icon name=\"certificate\" />\n                </template>\n              </van-field>\n            </van-cell-group>\n\n            <div class=\"login__submit\">\n              <van-button\n                :loading=\"isLoading\"\n                :disabled=\"isLoading\"\n                round\n                block\n                type=\"primary\"\n                native-type=\"submit\"\n                class=\"login__button\"\n              >\n                {{ isLoading ? \"注册中...\" : \"注册\" }}\n              </van-button>\n            </div>\n          </van-form>\n        </van-tab>\n      </van-tabs>\n    </main>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { showNotify } from \"vant\";\nimport type { FieldInstance, FieldRule } from \"vant\";\nimport { userApi } from \"@/api/user\";\nimport logo from \"@/assets/images/logo.png\";\nimport { STORAGE_KEYS } from \"@/constants/storage\";\n\n// 类型定义\ninterface LoginForm {\n  username: string;\n  password: string;\n}\n\ninterface RegisterForm {\n  username: string;\n  password: string;\n  confirmPassword: string;\n  registerCode: string;\n}\n\n// 响应式数据\nconst activeTab = ref(\"login\");\nconst isLoading = ref(false);\nconst loginPasswordRef = ref<FieldInstance>();\nconst rememberPassword = ref(false);\n\nconst loginForm = ref<LoginForm>({\n  username: \"\",\n  password: \"\",\n});\n\nconst registerForm = ref<RegisterForm>({\n  username: \"\",\n  password: \"\",\n  confirmPassword: \"\",\n  registerCode: \"\",\n});\n\n// 工具函数\nconst router = useRouter();\n\n// 方法定义\nconst focusLoginPassword = () => {\n  loginPasswordRef.value?.focus();\n};\n\n// 表单验证\nconst validateConfirmPassword = (value: string) => {\n  return value === registerForm.value.password;\n};\n\n// 在组件加载时检查是否有保存的账号密码\nonMounted(() => {\n  const savedUsername = localStorage.getItem(STORAGE_KEYS.USERNAME);\n  const savedPassword = localStorage.getItem(STORAGE_KEYS.PASSWORD);\n  if (savedUsername && savedPassword) {\n    loginForm.value.username = savedUsername;\n    loginForm.value.password = savedPassword;\n    rememberPassword.value = true;\n  }\n});\n\n// 登录处理\nconst handleLogin = async () => {\n  try {\n    isLoading.value = true;\n    const res = await userApi.login(loginForm.value);\n\n    if (res.code === 0) {\n      if (rememberPassword.value) {\n        localStorage.setItem(STORAGE_KEYS.USERNAME, loginForm.value.username);\n        localStorage.setItem(STORAGE_KEYS.PASSWORD, loginForm.value.password);\n      } else {\n        localStorage.removeItem(STORAGE_KEYS.USERNAME);\n        localStorage.removeItem(STORAGE_KEYS.PASSWORD);\n      }\n\n      localStorage.setItem(STORAGE_KEYS.TOKEN, res.data.token);\n      await router.push(\"/\");\n    } else {\n      showNotify({ type: \"danger\", message: res.message || \"登录失败\" });\n    }\n  } catch (error) {\n    showNotify({ type: \"danger\", message: \"登录失败\" });\n  } finally {\n    isLoading.value = false;\n  }\n};\n\n// 注册处理\nconst handleRegister = async () => {\n  try {\n    isLoading.value = true;\n    const res = await userApi.register({\n      username: registerForm.value.username,\n      password: registerForm.value.password,\n      registerCode: registerForm.value.registerCode,\n    });\n\n    if (res.code === 0) {\n      showNotify({ type: \"success\", message: \"注册成功\" });\n      // 自动填充登录表单\n      loginForm.value.username = registerForm.value.username;\n      loginForm.value.password = registerForm.value.password;\n      activeTab.value = \"login\";\n      // 清空注册表单\n      registerForm.value = {\n        username: \"\",\n        password: \"\",\n        confirmPassword: \"\",\n        registerCode: \"\",\n      };\n    } else {\n      showNotify({ type: \"danger\", message: res.message || \"注册失败\" });\n    }\n  } catch (error) {\n    showNotify({ type: \"danger\", message: \"注册失败\" });\n  } finally {\n    isLoading.value = false;\n  }\n};\n\n// 定义验证规则\nconst usernameRules: FieldRule[] = [\n  { required: true, message: \"请填写用户名\" },\n  { pattern: /.{3,}/, message: \"用户名至少3个字符\" },\n];\n\nconst passwordRules: FieldRule[] = [\n  { required: true, message: \"请填写密码\" },\n  { pattern: /.{6,}/, message: \"密码至少6个字符\" },\n];\n\nconst confirmPasswordRules: FieldRule[] = [\n  { required: true, message: \"请确认密码\" },\n  { validator: validateConfirmPassword, message: \"两次密码不一致\" },\n];\n\nconst registerCodeRules: FieldRule[] = [{ required: true, message: \"请填写注册码\" }];\n</script>\n\n<style lang=\"scss\" scoped>\n.login {\n  position: relative;\n  height: 100vh;\n  width: 100%;\n  overflow: hidden;\n\n  // 背景\n  &__background {\n    position: absolute;\n    inset: 0;\n    background: url(\"@/assets/images/mobile-login-bg.png\") no-repeat;\n    background-size: 100% auto;\n    filter: blur(1px);\n  }\n\n  // 主内容区\n  &__content {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    min-height: 65%;\n    padding: 40px 20px;\n    background-color: var(--theme-other_background);\n    border-radius: 24px 24px 0 0;\n    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1);\n    backdrop-filter: blur(8px);\n  }\n\n  // 头部\n  &__header {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin-bottom: 40px;\n  }\n\n  &__logo {\n    width: 60px;\n    height: 60px;\n    margin-right: 12px;\n    object-fit: contain;\n  }\n\n  &__title {\n    margin: 0;\n    font-size: 28px;\n    font-weight: 600;\n    color: var(--theme-theme);\n  }\n\n  // 表单\n  &__form {\n    padding: 0;\n    margin-top: 20px;\n  }\n\n  &__form-group {\n    margin: 0 12px;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n    border-radius: 12px;\n    overflow: hidden;\n  }\n\n  &__submit {\n    margin: 32px 12px 0;\n  }\n\n  &__button {\n    height: 48px;\n    font-size: 16px;\n    font-weight: 500;\n  }\n\n  // 记住密码区域\n  &__remember {\n    padding: 12px 16px;\n    border-top: 0.5px solid #f5f5f5;\n  }\n\n  &__tabs {\n    :deep() {\n      .van-tabs__wrap {\n        padding: 0 12px;\n      }\n\n      .van-tabs__nav {\n        background: transparent;\n      }\n\n      .van-tab {\n        color: var(--theme-color);\n        font-size: 16px;\n      }\n\n      .van-tab--active {\n        color: var(--theme-theme);\n        font-weight: 500;\n      }\n\n      .van-tabs__line {\n        background-color: var(--theme-theme);\n      }\n    }\n  }\n}\n\n// Vant 组件样式优化\n:deep(.van-cell-group--inset) {\n  margin: 0;\n}\n\n:deep(.van-field) {\n  padding: 16px;\n\n  .van-field__label {\n    width: 4em;\n    color: var(--theme-color);\n    font-size: 15px;\n  }\n\n  .van-field__value {\n    .van-field__body {\n      input {\n        font-size: 15px;\n      }\n    }\n  }\n\n  .van-field__left-icon {\n    margin-right: 8px;\n    font-size: 18px;\n  }\n}\n\n// 记住密码复选框样式优化\n:deep(.remember-checkbox) {\n  display: flex;\n  align-items: center;\n  font-size: 14px;\n  color: #666;\n\n  .van-checkbox__icon {\n    font-size: 16px;\n\n    .van-icon {\n      border: 1px solid #dcdee0;\n      border-radius: 2px;\n      transition: all 0.2s;\n    }\n  }\n\n  .van-checkbox__label {\n    margin-left: 6px;\n    line-height: 1;\n  }\n\n  &.van-checkbox--checked {\n    .van-icon {\n      background-color: var(--theme-theme);\n      border-color: var(--theme-theme);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/mobile/ResourceList.vue",
    "content": "<template>\n  <div ref=\"listRef\" class=\"resource-list\">\n    <!-- 头部刷新区 -->\n    <van-cell-group :border=\"false\" class=\"resource-list__header\">\n      <van-cell center clickable @click=\"refreshResources\">\n        <template #icon>\n          <van-icon name=\"replay\" class=\"header__icon\" />\n        </template>\n        <template #title>\n          <div class=\"header__content\">\n            <span class=\"content__title\">最新资源</span>\n            <span class=\"content__tip\">(点击可获取最新资源)</span>\n          </div>\n        </template>\n        <template #label>\n          <span class=\"header__time\">上次刷新：{{ resourceStore.lastUpdateTime }}</span>\n        </template>\n      </van-cell>\n    </van-cell-group>\n\n    <!-- 资源列表 -->\n    <van-tabs\n      v-model:active=\"currentTab\"\n      swipeable\n      animated\n      class=\"resource-list__tabs\"\n      :border=\"false\"\n    >\n      <van-tab\n        v-for=\"item in resourceStore.resources\"\n        :key=\"item.id\"\n        :name=\"item.id\"\n        :title=\"item.channelInfo.name\"\n      >\n        <ResourceCard\n          :current-channel-id=\"currentTab\"\n          @save=\"handleSave\"\n          @jump=\"handleJump\"\n          @search-moviefor-tag=\"searchMovieforTag\"\n        />\n      </van-tab>\n    </van-tabs>\n\n    <!-- 保存弹窗 -->\n    <van-popup\n      v-model:show=\"saveDialogVisible\"\n      round\n      closeable\n      position=\"bottom\"\n      :style=\"{ height: '80%', transform: 'translateZ(1px)' }\"\n      class=\"save-popup\"\n    >\n      <div class=\"save-popup__container\">\n        <!-- 弹窗头部 -->\n        <div class=\"save-popup__header\">\n          <van-tag :color=\"getTagColor(currentResource?.cloudType)\" round size=\"medium\">\n            {{ currentResource?.cloudType }}\n          </van-tag>\n          <span class=\"header__title\">{{ saveDialogMap[saveDialogStep].title }}</span>\n          <span\n            v-if=\"resourceStore.shareInfo.fileSize && saveDialogStep === 1\"\n            class=\"header__size\"\n          >\n            {{ formattedFileSize(resourceStore.shareInfo.fileSize) }}\n          </span>\n        </div>\n\n        <!-- 弹窗内容 -->\n        <div class=\"save-popup__content\">\n          <van-empty v-if=\"resourceStore.loadTree && saveDialogStep === 1\" class=\"content__loading\">\n            <template #image>\n              <van-loading size=\"24px\" vertical>加载中...</van-loading>\n            </template>\n          </van-empty>\n\n          <resource-select\n            v-if=\"saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length\"\n            :cloud-type=\"currentResource?.cloudType\"\n          />\n\n          <folder-select\n            v-if=\"saveDialogVisible && saveDialogStep === 2 && currentResource\"\n            :cloud-type=\"currentResource.cloudType\"\n            @select=\"handleFolderSelect\"\n            @close=\"saveDialogVisible = false\"\n          />\n        </div>\n\n        <!-- 弹窗底部 -->\n        <div class=\"save-popup__footer\">\n          <van-cell class=\"footer__path\" :border=\"false\">\n            <template #title>\n              <div class=\"path__label\">保存至：</div>\n            </template>\n            <template #value>\n              <div class=\"path__value\">\n                <van-icon name=\"folder-o\" class=\"value__icon\" />\n                <span\n                  class=\"value__text\"\n                  :class=\"{ 'value__text--placeholder': !currentFolderPath }\"\n                >\n                  {{ getCurrentFolderName }}\n                </span>\n              </div>\n            </template>\n          </van-cell>\n\n          <van-button\n            round\n            block\n            type=\"primary\"\n            size=\"large\"\n            :loading=\"isLoading\"\n            @click=\"handleConfirmClick\"\n          >\n            {{ saveDialogMap[saveDialogStep].buttonText }}\n          </van-button>\n        </div>\n      </div>\n    </van-popup>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onUnmounted, computed, onBeforeUnmount } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { showToast } from \"vant\";\nimport { useResourceStore } from \"@/stores/resource\";\nimport { formattedFileSize, throttle } from \"@/utils/index\";\nimport type { Folder, ResourceItem } from \"@/types\";\nimport FolderSelect from \"@/components/mobile/FolderSelect.vue\";\nimport ResourceSelect from \"@/components/mobile/ResourceSelect.vue\";\nimport ResourceCard from \"@/components/mobile/ResourceCard.vue\";\n\n// 状态管理\nconst router = useRouter();\nconst resourceStore = useResourceStore();\n\n// 响应式数据\nconst saveDialogVisible = ref(false);\nconst currentResource = ref<ResourceItem | null>(null);\nconst currentFolderId = ref<string | null>(null);\nconst currentFolderPath = ref<Folder[] | null>(null);\nconst saveDialogStep = ref<1 | 2>(1);\nconst currentTab = ref<string>(\"\");\nconst isLoading = ref(false);\nconst listRef = ref<HTMLElement | null>(null);\n\n// 计算属性\nconst getCurrentFolderName = computed(() => {\n  return currentFolderPath.value\n    ? currentFolderPath.value[currentFolderPath.value.length - 1]?.name\n    : \"待选择\";\n});\n\n// 常量定义\nconst saveDialogMap = {\n  1: { title: \"选择资源\", buttonText: \"下一步\" },\n  2: { title: \"选择保存目录\", buttonText: \"保存\" },\n};\n\n// 标签颜色映射\nconst getTagColor = (type?: string) => {\n  const colorMap: Record<string, string> = {\n    pan115: \"#07c160\",\n    quark: \"#1989fa\",\n  };\n  return colorMap[type || \"\"] || \"#ff976a\";\n};\n\n// 监听器\nwatch(\n  () => resourceStore.resources,\n  () => {\n    if (resourceStore.resources.length > 0) {\n      currentTab.value = resourceStore.resources[0].id;\n    }\n  }\n);\n\n// 方法定义\nconst refreshResources = () => {\n  resourceStore.searchResources(\"\", false);\n};\n\nconst handleSave = async (resource: ResourceItem) => {\n  currentResource.value = resource;\n  saveDialogVisible.value = true;\n  saveDialogStep.value = 1;\n\n  if (!(await resourceStore.getResourceListAndSelect(resource))) {\n    saveDialogVisible.value = false;\n  }\n};\n\nconst handleJump = (resource: ResourceItem) => {\n  window.open(resource.cloudLinks[0], \"_blank\");\n};\n\nconst handleFolderSelect = (folders: Folder[] | null) => {\n  if (!currentResource.value) return;\n  currentFolderPath.value = folders;\n  currentFolderId.value = folders?.[folders.length - 1]?.cid || \"0\";\n};\n\nconst handleConfirmClick = async () => {\n  if (saveDialogStep.value === 1) {\n    if (!resourceStore.resourceSelect.some((x) => x.isChecked)) {\n      showToast(\"请选择要保存的资源\");\n      return;\n    }\n    saveDialogStep.value = 2;\n  } else {\n    handleSaveBtnClick();\n  }\n};\n\nconst handleSaveBtnClick = async () => {\n  if (!currentResource.value || !currentFolderId.value) return;\n  saveDialogVisible.value = false;\n  await resourceStore.saveResource(currentResource.value, currentFolderId.value);\n};\n\nconst searchMovieforTag = (tag: string) => {\n  router.push({ path: \"/resource\", query: { keyword: tag } });\n};\n\n// 使用节流包装加载更多函数\nconst throttledLoadMore = throttle((channelId: string) => {\n  resourceStore.searchResources(\"\", true, channelId);\n}, 2000);\n\n// 滚动加载\nconst doScroll = () => {\n  const appElement = document.querySelector(\"#app\") as HTMLElement;\n  if (appElement) {\n    const { scrollHeight, scrollTop, clientHeight } = appElement;\n    if (scrollHeight - (clientHeight + scrollTop) <= 1) {\n      throttledLoadMore(currentTab.value);\n    }\n  }\n};\n\n// 生命周期\nonMounted(() => {\n  const appElement = document.querySelector(\"#app\");\n  if (appElement) {\n    appElement.addEventListener(\"scroll\", doScroll);\n  }\n});\n\nonUnmounted(() => {\n  const appElement = document.querySelector(\"#app\");\n  if (appElement) {\n    appElement.removeEventListener(\"scroll\", doScroll);\n  }\n});\n\n// 监听标签页切换\nwatch(currentTab, () => {\n  const appElement = document.querySelector(\"#app\");\n  if (appElement) {\n    appElement.scrollTo(0, 0);\n  }\n});\n// 页面进入 设置缓存的数据源\nonMounted(() => {\n  const lastResourceList = localStorage.getItem(\"last_resource_list\");\n  if (lastResourceList) {\n    resourceStore.resources = JSON.parse(lastResourceList).list;\n  }\n});\n\n// 页面销毁 清除搜索词\nonBeforeUnmount(() => {\n  resourceStore.keyword = \"\";\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.resource-list {\n  min-height: 100%;\n  background: var(--van-background);\n  padding-bottom: 20px;\n\n  &__header {\n    margin-bottom: 8px;\n    background: var(--theme-other_background);\n\n    :deep(.van-cell) {\n      padding: 12px 16px;\n      min-height: 24px;\n    }\n\n    .header__icon {\n      font-size: 30px;\n      color: var(--theme-theme);\n      margin-right: 10px;\n      line-height: 1;\n    }\n\n    .header__content {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n\n      .content__title {\n        font-size: 15px;\n        font-weight: 500;\n        line-height: 1.4;\n      }\n\n      .content__tip {\n        font-size: 12px;\n        color: var(--van-gray-6);\n        background: var(--van-gray-1);\n        padding: 2px 6px;\n        border-radius: 4px;\n        line-height: 1.4;\n      }\n    }\n\n    .header__time {\n      font-size: 12px;\n      color: var(--van-gray-6);\n      line-height: 1.4;\n      margin-top: 2px;\n    }\n  }\n\n  &__tabs {\n    :deep(.van-tabs__wrap) {\n      background: var(--theme-other_background);\n    }\n\n    :deep(.van-tab) {\n      font-size: 14px;\n      padding: 0 20px;\n      height: 44px;\n      line-height: 44px;\n    }\n\n    :deep(.van-tabs__line) {\n      background: var(--theme-theme);\n    }\n\n    :deep(.van-tabs__content) {\n      padding: 8px 0;\n    }\n  }\n}\n\n.save-popup {\n  &__container {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    padding-bottom: calc(env(safe-area-inset-bottom) + 50px);\n  }\n\n  &__header {\n    flex-shrink: 0;\n    padding: 16px;\n    border-bottom: 0.5px solid var(--van-gray-3);\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding-right: 40px;\n\n    .header__title {\n      font-size: 16px;\n      font-weight: 500;\n    }\n\n    .header__size {\n      margin-left: auto;\n      font-size: 13px;\n      color: var(--van-gray-6);\n      max-width: 120px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n  }\n\n  &__content {\n    flex: 1;\n    overflow-y: auto;\n    background: var(--van-background-2);\n\n    .content__loading {\n      padding: 32px 0;\n    }\n  }\n\n  &__footer {\n    flex-shrink: 0;\n    padding: 12px 16px 16px;\n    background: var(--theme-other_background);\n    border-top: 0.5px solid var(--van-gray-3);\n    padding-bottom: calc(16px + env(safe-area-inset-bottom));\n\n    .footer__path {\n      margin: 0 0 12px;\n\n      :deep(.van-cell__title) {\n        flex: none;\n        padding-right: 8px;\n        display: flex;\n        align-items: center;\n      }\n\n      :deep(.van-cell__value) {\n        flex: 1;\n      }\n\n      .path__label {\n        font-size: 14px;\n        color: var(--van-text-color);\n        font-weight: 500;\n        white-space: nowrap;\n      }\n\n      .path__value {\n        display: flex;\n        align-items: center;\n        gap: 4px;\n        padding: 6px 12px;\n        background: var(--van-gray-1);\n        border-radius: 4px;\n        width: 100%;\n        box-sizing: border-box;\n\n        .value__icon {\n          font-size: 16px;\n          color: var(--theme-theme);\n          flex-shrink: 0;\n        }\n\n        .value__text {\n          font-size: 14px;\n          color: var(--van-text-color);\n          flex: 1;\n          min-width: 0;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          white-space: nowrap;\n          text-align: left;\n          display: block;\n\n          &--placeholder {\n            color: var(--van-gray-6);\n          }\n        }\n      }\n    }\n\n    :deep(.van-cell__value) {\n      flex: 1;\n      text-align: left;\n    }\n\n    .van-button {\n      height: 44px;\n      font-size: 16px;\n      font-weight: 500;\n    }\n  }\n\n  :deep(.van-popup) {\n    z-index: 2001 !important;\n  }\n}\n\n// 全局样式优化\n:deep(.van-cell) {\n  padding: 16px 20px;\n\n  &::after {\n    display: none;\n  }\n}\n\n:deep(.van-popup) {\n  max-height: 90vh;\n}\n\n:deep(.van-overlay) {\n  background-color: rgba(0, 0, 0, 0.7);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/mobile/Setting.vue",
    "content": "<template>\n  <div class=\"setting\">\n    <!-- 全局设置 -->\n    <div v-if=\"settingStore.globalSetting\" class=\"setting__section\">\n      <div class=\"setting__title\">项目配置</div>\n      <div class=\"setting__card\">\n        <van-cell-group inset>\n          <van-field\n            v-model=\"localGlobalSetting.httpProxyHost\"\n            label=\"代理服务器IP\"\n            placeholder=\"127.0.0.1\"\n            @update:model-value=\"handleProxyHostChange\"\n          />\n          <van-field\n            v-model=\"localGlobalSetting.httpProxyPort\"\n            label=\"代理端口\"\n            placeholder=\"7890\"\n          />\n          <van-field\n            v-model.number=\"localGlobalSetting.AdminUserCode\"\n            label=\"管理员码\"\n            type=\"digit\"\n            placeholder=\"设置管理员注册码\"\n          />\n          <van-field\n            v-model.number=\"localGlobalSetting.CommonUserCode\"\n            label=\"用户注册码\"\n            type=\"digit\"\n            placeholder=\"设置普通用户注册码\"\n          />\n          <van-cell center title=\"启用代理\">\n            <template #right-icon>\n              <van-switch\n                v-model=\"localGlobalSetting.isProxyEnabled\"\n                size=\"24px\"\n                @change=\"handleProxyChange\"\n              />\n            </template>\n          </van-cell>\n        </van-cell-group>\n      </div>\n    </div>\n\n    <!-- 用户设置 -->\n    <div class=\"setting__section\">\n      <div class=\"setting__title\">用户配置</div>\n      <div class=\"setting__card\">\n        <van-cell-group inset>\n          <van-field\n            v-model=\"localUserSettings.cloud115Cookie\"\n            :type=\"showCloud115Cookie ? 'text' : 'password'\"\n            label=\"115网盘\"\n            rows=\"2\"\n            autosize\n            placeholder=\"请输入115网盘Cookie\"\n          >\n            <template #right-icon>\n              <van-icon\n                :name=\"showCloud115Cookie ? 'eye-o' : 'closed-eye'\"\n                @click=\"showCloud115Cookie = !showCloud115Cookie\"\n              />\n            </template>\n          </van-field>\n\n          <van-field\n            v-model=\"localUserSettings.quarkCookie\"\n            :type=\"showQuarkCookie ? 'text' : 'password'\"\n            label=\"夸克网盘\"\n            rows=\"2\"\n            autosize\n            placeholder=\"请输入夸克网盘Cookie\"\n          >\n            <template #right-icon>\n              <van-icon\n                :name=\"showQuarkCookie ? 'eye-o' : 'closed-eye'\"\n                @click=\"showQuarkCookie = !showQuarkCookie\"\n              />\n            </template>\n          </van-field>\n        </van-cell-group>\n      </div>\n\n      <!-- 帮助说明 -->\n      <div class=\"setting__help\">\n        <div class=\"help__title\">帮助说明</div>\n        <div class=\"help__links\">\n          <van-cell\n            title=\"CloudSaver部署与使用常见问题\"\n            is-link\n            url=\"https://www.yuque.com/xiaoruihenbangde/ggogn3/ga6gaaiy5fsyw62l?singleDoc=true\"\n          />\n          <van-cell\n            title=\"CloudSaver功能介绍\"\n            is-link\n            url=\"https://www.yuque.com/xiaoruihenbangde/ggogn3/cl2g0p9h3xrgfa5i\"\n          />\n          <van-cell\n            title=\"如何获取115网盘cookie？\"\n            is-link\n            url=\"https://alist.nn.ci/zh/guide/drivers/115.html\"\n          />\n          <van-cell\n            title=\"如何获取夸克网盘cookie？\"\n            is-link\n            url=\"https://alist.nn.ci/zh/guide/drivers/quark.html\"\n          />\n        </div>\n      </div>\n    </div>\n\n    <!-- 保存按钮 -->\n    <div class=\"setting__submit\">\n      <van-button round block type=\"primary\" @click=\"handleSave\"> 保存设置 </van-button>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useUserSettingStore } from \"@/stores/userSetting\";\nimport { ref, watch } from \"vue\";\nimport { showNotify } from \"vant\";\nimport type { GlobalSettingAttributes, UserSettingAttributes } from \"@/types/user\";\n\nconst settingStore = useUserSettingStore();\n\n// 本地状态\nconst localGlobalSetting = ref<GlobalSettingAttributes>({\n  httpProxyHost: \"127.0.0.1\",\n  httpProxyPort: \"7890\",\n  isProxyEnabled: false,\n  AdminUserCode: 230713,\n  CommonUserCode: 9527,\n});\n\nconst localUserSettings = ref<UserSettingAttributes>({\n  cloud115Cookie: \"\",\n  quarkCookie: \"\",\n});\n\n// 添加显示/隐藏密码的状态\nconst showCloud115Cookie = ref(false);\nconst showQuarkCookie = ref(false);\n\n// 监听 store 变化\nwatch(\n  () => settingStore.globalSetting,\n  (newVal) => {\n    if (newVal) {\n      localGlobalSetting.value = { ...newVal };\n    }\n  },\n  { immediate: true }\n);\n\nwatch(\n  () => settingStore.userSettings,\n  (newVal) => {\n    if (newVal) {\n      localUserSettings.value = { ...newVal };\n    }\n  },\n  { immediate: true }\n);\n\n// 初始化获取设置\nsettingStore.getSettings();\n\n// 处理代理开关变化并立即保存\nconst handleProxyChange = async (val: boolean) => {\n  try {\n    localGlobalSetting.value.isProxyEnabled = val;\n    await settingStore.saveSettings({\n      globalSetting: localGlobalSetting.value,\n      userSettings: localUserSettings.value,\n    });\n    showNotify({ type: \"success\", message: \"代理设置已更新\" });\n  } catch (error) {\n    showNotify({ type: \"danger\", message: \"代理设置更新失败\" });\n    // 保存失败时恢复开关状态\n    localGlobalSetting.value.isProxyEnabled = !val;\n  }\n};\n\n// 其他设置的保存\nconst handleSave = async () => {\n  try {\n    await settingStore.saveSettings({\n      globalSetting: localGlobalSetting.value,\n      userSettings: localUserSettings.value,\n    });\n    showNotify({ type: \"success\", message: \"设置保存成功\" });\n  } catch (error) {\n    showNotify({ type: \"danger\", message: \"设置保存失败\" });\n  }\n};\n\n// 处理代理地址,去除协议前缀\nconst handleProxyHostChange = (val: string) => {\n  // 移除 http:// 或 https:// 前缀\n  const cleanHost = val.replace(/^(https?:\\/\\/)/i, \"\");\n  // 更新状态\n  localGlobalSetting.value.httpProxyHost = cleanHost;\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.setting {\n  min-height: 100vh;\n  background: var(--theme-background);\n  padding: var(--spacing-base);\n  padding-bottom: 90px; // 为底部导航栏和按钮留出空间\n\n  &__section {\n    margin-bottom: var(--spacing-lg);\n  }\n\n  &__title {\n    font-size: 16px; // 统一字体大小\n    font-weight: 500;\n    margin-bottom: var(--spacing-base);\n    color: var(--theme-color);\n  }\n\n  &__card {\n    background: var(--theme-other_background);\n    border-radius: var(--border-radius-lg);\n    overflow: hidden;\n  }\n\n  &__help {\n    margin-top: var(--spacing-base);\n\n    .help__title {\n      font-size: 14px; // 统一字体大小\n      margin-bottom: var(--spacing-sm);\n      color: var(--theme-color);\n    }\n  }\n\n  &__submit {\n    position: fixed;\n    left: 0;\n    right: 0;\n    bottom: 50px; // tabbar 高度\n    padding: var(--spacing-base);\n    background: var(--theme-other_background);\n    z-index: 99;\n  }\n}\n\n// 深度修改 Vant 组件样式\n:deep(.van-field) {\n  font-size: 14px; // 统一字体大小\n}\n\n:deep(.van-field__label) {\n  width: 6em;\n  color: var(--theme-color);\n}\n\n:deep(.van-cell) {\n  font-size: 14px; // 统一字体大小\n  padding: 12px var(--spacing-base);\n}\n\n:deep(.van-button) {\n  height: 40px; // 统一按钮高度\n  font-size: 14px; // 统一字体大小\n}\n\n:deep(.van-cell-group--inset) {\n  margin: 0;\n}\n\n// 添加图标样式\n:deep(.van-field__right-icon) {\n  padding: 0 8px;\n  cursor: pointer;\n  color: var(--theme-color);\n\n  .van-icon {\n    font-size: 18px;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/pc/Login.vue",
    "content": "<template>\n  <div class=\"login-page\">\n    <div class=\"login-bg\"></div>\n    <div class=\"login-card\">\n      <div class=\"card-header\">\n        <img src=\"@/assets/images/logo.png\" alt=\"Logo\" class=\"logo\" />\n        <h1 class=\"title\">欢迎回来</h1>\n        <p class=\"subtitle\">登录您的账户以继续</p>\n      </div>\n\n      <el-tabs v-model=\"activeTab\" class=\"login-tabs\">\n        <el-tab-pane label=\"登录\" name=\"login\">\n          <el-form\n            ref=\"loginFormRef\"\n            :model=\"loginForm\"\n            :rules=\"loginRules\"\n            @keyup.enter=\"handleLogin\"\n          >\n            <el-form-item prop=\"username\">\n              <el-input\n                v-model=\"loginForm.username\"\n                placeholder=\"用户名\"\n                :prefix-icon=\"User\"\n                autocomplete=\"username\"\n              />\n            </el-form-item>\n\n            <el-form-item prop=\"password\">\n              <el-input\n                v-model=\"loginForm.password\"\n                type=\"password\"\n                placeholder=\"密码\"\n                :prefix-icon=\"Lock\"\n                show-password\n                autocomplete=\"current-password\"\n              />\n            </el-form-item>\n\n            <div class=\"form-options\">\n              <el-checkbox v-model=\"rememberPassword\">记住密码</el-checkbox>\n            </div>\n\n            <el-button type=\"primary\" class=\"submit-btn\" :loading=\"loading\" @click=\"handleLogin\">\n              登录\n            </el-button>\n          </el-form>\n        </el-tab-pane>\n\n        <el-tab-pane label=\"注册\" name=\"register\">\n          <el-form ref=\"registerFormRef\" :model=\"registerForm\" :rules=\"registerRules\">\n            <el-form-item prop=\"username\">\n              <el-input v-model=\"registerForm.username\" placeholder=\"用户名\" :prefix-icon=\"User\" />\n            </el-form-item>\n\n            <el-form-item prop=\"password\">\n              <el-input\n                v-model=\"registerForm.password\"\n                type=\"password\"\n                placeholder=\"密码\"\n                :prefix-icon=\"Lock\"\n                show-password\n              />\n            </el-form-item>\n\n            <el-form-item prop=\"confirmPassword\">\n              <el-input\n                v-model=\"registerForm.confirmPassword\"\n                type=\"password\"\n                placeholder=\"确认密码\"\n                :prefix-icon=\"Lock\"\n                show-password\n              />\n            </el-form-item>\n\n            <el-form-item prop=\"registerCode\">\n              <el-input\n                v-model=\"registerForm.registerCode\"\n                placeholder=\"注册码\"\n                :prefix-icon=\"Key\"\n              />\n            </el-form-item>\n\n            <el-button type=\"primary\" class=\"submit-btn\" :loading=\"loading\" @click=\"handleRegister\">\n              注册\n            </el-button>\n          </el-form>\n        </el-tab-pane>\n      </el-tabs>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { ElMessage } from \"element-plus\";\nimport { User, Lock, Key } from \"@element-plus/icons-vue\";\nimport { userApi } from \"@/api/user\";\nimport \"@/styles/common.scss\";\nimport { STORAGE_KEYS } from \"@/constants/storage\";\nimport type { FormItemRule } from \"element-plus\";\n\n// 状态\nconst activeTab = ref(\"login\");\nconst loading = ref(false);\nconst rememberPassword = ref(false);\n\nconst loginForm = ref({\n  username: \"\",\n  password: \"\",\n});\n\nconst registerForm = ref({\n  username: \"\",\n  password: \"\",\n  confirmPassword: \"\",\n  registerCode: \"\",\n});\n\n// 表单校验规则\nconst loginRules = {\n  username: [\n    { required: true, message: \"请输入用户名\", trigger: \"blur\" },\n    { min: 3, max: 20, message: \"长度在 3 到 20 个字符\", trigger: \"blur\" },\n  ],\n  password: [\n    { required: true, message: \"请输入密码\", trigger: \"blur\" },\n    { min: 6, max: 20, message: \"长度在 6 到 20 个字符\", trigger: \"blur\" },\n  ],\n};\n\nconst registerRules = {\n  ...loginRules,\n  confirmPassword: [\n    { required: true, message: \"请确认密码\", trigger: \"blur\" },\n    {\n      validator: (_rule: FormItemRule, value: string, callback: (error?: Error) => void) => {\n        if (value !== registerForm.value.password) {\n          callback(new Error(\"两次输入密码不一致\"));\n        } else {\n          callback();\n        }\n      },\n      trigger: \"blur\",\n    },\n  ],\n  registerCode: [{ required: true, message: \"请输入注册码\", trigger: \"blur\" }],\n};\n\nconst router = useRouter();\nconst loginFormRef = ref();\nconst registerFormRef = ref();\n\n// 记住密码相关\nonMounted(() => {\n  const savedUsername = localStorage.getItem(STORAGE_KEYS.USERNAME);\n  const savedPassword = localStorage.getItem(STORAGE_KEYS.PASSWORD);\n  if (savedUsername && savedPassword) {\n    loginForm.value.username = savedUsername;\n    loginForm.value.password = savedPassword;\n    rememberPassword.value = true;\n  }\n});\n\n// 登录处理\nconst handleLogin = async () => {\n  if (!loginFormRef.value) return;\n\n  await loginFormRef.value.validate(async (valid: boolean) => {\n    if (valid) {\n      loading.value = true;\n      try {\n        const res = await userApi.login(loginForm.value);\n        if (res.code === 0) {\n          // 记住密码\n          if (rememberPassword.value) {\n            localStorage.setItem(STORAGE_KEYS.USERNAME, loginForm.value.username);\n            localStorage.setItem(STORAGE_KEYS.PASSWORD, loginForm.value.password);\n          } else {\n            localStorage.removeItem(STORAGE_KEYS.USERNAME);\n            localStorage.removeItem(STORAGE_KEYS.PASSWORD);\n          }\n\n          localStorage.setItem(STORAGE_KEYS.TOKEN, res.data.token);\n          ElMessage.success(\"登录成功\");\n          router.push(\"/\");\n        } else {\n          ElMessage.error(res.message || \"登录失败\");\n        }\n      } catch (error: unknown) {\n        ElMessage.error(error instanceof Error ? error.message : \"登录失败\");\n      } finally {\n        loading.value = false;\n      }\n    }\n  });\n};\n\n// 注册处理\nconst handleRegister = async () => {\n  if (!registerFormRef.value) return;\n\n  await registerFormRef.value.validate(async (valid: boolean) => {\n    if (valid) {\n      loading.value = true;\n      try {\n        const res = await userApi.register({\n          username: registerForm.value.username,\n          password: registerForm.value.password,\n          registerCode: registerForm.value.registerCode,\n        });\n\n        if (res.code === 0) {\n          ElMessage.success(\"注册成功\");\n          // 自动填充登录表单\n          loginForm.value.username = registerForm.value.username;\n          loginForm.value.password = registerForm.value.password;\n          activeTab.value = \"login\";\n          // 自动登录\n          handleLogin();\n        } else {\n          ElMessage.error(res.message || \"注册失败\");\n        }\n      } catch (error: unknown) {\n        ElMessage.error(error instanceof Error ? error.message : \"注册失败\");\n      } finally {\n        loading.value = false;\n      }\n    }\n  });\n};\n</script>\n\n<style scoped lang=\"scss\">\n@import \"@/styles/common.scss\";\n\n.login-page {\n  @include flex-center;\n  min-height: 100vh;\n  background: var(--theme-bg);\n  position: relative;\n}\n\n.login-bg {\n  position: fixed;\n  inset: 0;\n  background-image: url(\"@/assets/images/login-bg.jpg\");\n  background-size: cover;\n  background-position: center;\n  filter: blur(3px);\n  z-index: 0;\n}\n\n.login-card {\n  @include glass-effect;\n  position: relative;\n  width: 420px;\n  padding: 32px;\n  border-radius: var(--theme-radius);\n  box-shadow: var(--theme-shadow);\n  z-index: 1;\n}\n\n.card-header {\n  text-align: center;\n  margin-bottom: 32px;\n\n  .logo {\n    width: 64px;\n    height: 64px;\n    margin-bottom: 16px;\n  }\n\n  .title {\n    font-size: 24px;\n    font-weight: 600;\n    color: var(--theme-text-primary);\n    margin-bottom: 8px;\n  }\n\n  .subtitle {\n    font-size: 14px;\n    color: var(--theme-text-secondary);\n  }\n}\n\n.login-tabs {\n  :deep(.el-tabs__nav-wrap::after) {\n    display: none;\n  }\n\n  :deep(.el-tabs__active-bar) {\n    background-color: var(--theme-primary);\n  }\n\n  :deep(.el-tabs__item) {\n    color: var(--theme-text-secondary);\n    font-size: 16px;\n\n    &.is-active {\n      color: var(--theme-primary);\n      font-weight: 500;\n    }\n  }\n}\n\n:deep(.el-form-item) {\n  margin-bottom: 24px;\n\n  .el-input__wrapper {\n    background-color: rgba(255, 255, 255, 0.5);\n    box-shadow: none;\n    border: 1px solid rgba(0, 0, 0, 0.1);\n    transition: var(--theme-transition);\n\n    &:hover {\n      border-color: var(--theme-primary);\n    }\n\n    &.is-focus {\n      border-color: var(--theme-primary);\n      box-shadow: 0 0 0 1px var(--theme-primary);\n    }\n  }\n\n  .el-input__inner {\n    height: 42px;\n  }\n}\n\n.form-options {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 24px;\n}\n\n.submit-btn {\n  width: 100%;\n  height: 42px;\n  font-size: 16px;\n  border-radius: var(--theme-radius-sm);\n  background: var(--theme-primary);\n  transition: var(--theme-transition);\n\n  &:hover {\n    background: var(--theme-primary-hover);\n  }\n\n  &:active {\n    transform: scale(0.98);\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"preserve\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"baseUrl\": \".\",\n    \"typeRoots\": [\"./node_modules/@types\", \"./types\"],\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.d.ts\", \"src/**/*.tsx\", \"src/**/*.vue\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport vue from \"@vitejs/plugin-vue\";\nimport { fileURLToPath, URL } from \"node:url\";\nimport AutoImport from \"unplugin-auto-import/vite\";\nimport Components from \"unplugin-vue-components/vite\";\nimport { ElementPlusResolver } from \"unplugin-vue-components/resolvers\";\nimport { VantResolver } from \"@vant/auto-import-resolver\";\nimport { VitePWA } from \"vite-plugin-pwa\";\nimport { resolve } from \"path\";\n\nexport default defineConfig({\n  base: \"/\",\n  plugins: [\n    vue(),\n    VitePWA({\n      registerType: \"autoUpdate\",\n      includeAssets: [\"logo-1.png\", \"logo.svg\"],\n      injectRegister: \"auto\",\n      workbox: {\n        globPatterns: [\"**/*.{js,css,html,png,svg}\"],\n      },\n      manifest: {\n        name: \"CloudSaver\",\n        short_name: \"CloudSaver\",\n        description: \"网盘资源搜索工具\",\n        theme_color: \"#ffffff\",\n        background_color: \"#ffffff\",\n        display: \"standalone\",\n        scope: \"/\",\n        start_url: \"/\",\n        icons: [\n          {\n            src: \"logo-1.png\",\n            sizes: \"192x192\",\n            type: \"image/png\",\n          },\n          {\n            src: \"logo.svg\",\n            sizes: \"192x192\",\n            type: \"image/svg+xml\",\n          },\n        ],\n      },\n    }),\n    AutoImport({\n      resolvers: [ElementPlusResolver(), VantResolver()],\n    }),\n    Components({\n      resolvers: [ElementPlusResolver(), VantResolver()],\n    }),\n  ],\n  css: {\n    preprocessorOptions: {\n      scss: {\n        additionalData: `@use \"@/styles/global.scss\";`,\n      },\n    },\n  },\n  resolve: {\n    alias: {\n      \"@\": resolve(__dirname, \"src\"),\n    },\n  },\n  server: {\n    host: \"0.0.0.0\",\n    port: 8008,\n    proxy: {\n      \"/api\": {\n        target: process.env.VITE_API_BASE_URL_PROXY || \"http://127.0.0.1:8009\",\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/api/, \"\"),\n        configure: (proxy, _options) => {\n          proxy.on(\"error\", (err, _req, _res) => {\n            console.log(\"proxy error\", err);\n          });\n          proxy.on(\"proxyReq\", (proxyReq, req, _res) => {\n            console.log(\"Sending Request:\", req.method, req.url);\n          });\n          proxy.on(\"proxyRes\", (proxyRes, req, _res) => {\n            console.log(\"Received Response:\", proxyRes.statusCode, req.url);\n          });\n        },\n      },\n      \"/tele-images\": {\n        target: process.env.VITE_API_BASE_URL_PROXY || \"http://127.0.0.1:8009\",\n        changeOrigin: true,\n      },\n    },\n  },\n  build: {\n    outDir: \"dist\",\n    assetsDir: \"assets\",\n    rollupOptions: {\n      input: {\n        main: fileURLToPath(new URL(\"./index.html\", import.meta.url)),\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "nginx.conf",
    "content": "# nginx.conf\nuser root;  # 定义 Nginx 进程的运行用户\nworker_processes 1;  # 设置 Nginx 进程数\n\nevents {\n    worker_connections 1024;  # 每个 worker 进程最大连接数\n}\n\nhttp {\n   include       mime.types;\n    default_type  application/octet-stream;\n    server {\n        listen 8008;\n        server_name localhost;\n\n        location / {\n            root /usr/share/nginx/html;\n            try_files $uri $uri/ /index.html;\n        }\n\n        location /api/ {\n            rewrite ^/api/(.*)$ /$1 break;\n            proxy_pass http://localhost:8009;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n        }\n        \n        location /tele-images/ {\n            proxy_pass http://localhost:8009;\n            proxy_set_header Host $host;\n        }\n    }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"cloud-saver\",\n  \"version\": \"0.2.5\",\n  \"private\": true,\n  \"workspaces\": [\n    \"frontend\",\n    \"backend\"\n  ],\n  \"scripts\": {\n    \"ins\": \"npm-run-all --parallel install:*\",\n    \"install:frontend\": \"cd frontend && npm install\",\n    \"install:backend\": \"cd backend && npm install\",\n    \"dev\": \"npm-run-all --parallel dev:*\",\n    \"dev:frontend\": \"cd frontend && npm run dev\",\n    \"dev:backend\": \"cd backend && npm run dev\",\n    \"build\": \"npm-run-all --parallel build:*\",\n    \"build:frontend\": \"cd frontend && npm run build\",\n    \"build:backend\": \"cd backend && npm run build\",\n    \"clean\": \"rimraf **/node_modules **/dist\",\n    \"version:patch\": \"npm version patch -w frontend && npm version patch\",\n    \"version:minor\": \"npm version minor -w frontend && npm version minor\",\n    \"version:major\": \"npm version major -w frontend && npm version major\",\n    \"format\": \"prettier --write \\\"**/*.{js,ts,vue,json,css,scss}\\\"\",\n    \"format:check\": \"prettier --check \\\"**/*.{js,ts,vue,json,css,scss}\\\"\",\n    \"format:all\": \"npm run format && npm run lint:fix\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx\",\n    \"lint:fix\": \"eslint . --ext .js,.jsx,.ts,.tsx --fix\"\n  },\n  \"devDependencies\": {\n    \"@typescript-eslint/eslint-plugin\": \"^7.1.0\",\n    \"@typescript-eslint/parser\": \"^7.1.0\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-prettier\": \"^10.0.1\",\n    \"eslint-plugin-prettier\": \"^5.2.3\",\n    \"eslint-plugin-vue\": \"^9.32.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^3.4.2\",\n    \"rimraf\": \"^5.0.5\",\n    \"vue-eslint-parser\": \"^9.4.3\"\n  },\n  \"engines\": {\n    \"pnpm\": \">=6.0.0\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"@parcel/watcher\",\n      \"bcrypt\",\n      \"esbuild\",\n      \"sqlite3\",\n      \"vue-demi\"\n    ]\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'frontend'\n  - 'backend'"
  }
]