Repository: 2977094657/BiliHistoryFrontend Branch: master Commit: ac05f46d3ccd Files: 104 Total size: 1.0 MB Directory structure: gitextract_3n4s52y9/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── 问题反馈-功能请求.md │ └── workflows/ │ ├── docker-image.yml │ └── tauri-release.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── deploy/ │ └── Caddyfile ├── index.html ├── jsconfig.json ├── package.json ├── postcss.config.js ├── src/ │ ├── App.vue │ ├── api/ │ │ └── api.js │ ├── components/ │ │ ├── PrivacyControl.vue │ │ └── tailwind/ │ │ ├── ArtPlayerWithDanmaku.vue │ │ ├── BusinessTypeSelector.vue │ │ ├── CustomDropdown.vue │ │ ├── DataSyncManager.vue │ │ ├── DownloadDialog.vue │ │ ├── EnvironmentCheck.vue │ │ ├── FavoriteDialog.vue │ │ ├── FilterDropdown.vue │ │ ├── HistoryContent.vue │ │ ├── LoginDialog.vue │ │ ├── Navbar.vue │ │ ├── Pagination.vue │ │ ├── SearchBar.vue │ │ ├── Settings.vue │ │ ├── Sidebar.vue │ │ ├── SimpleSearchBar.vue │ │ ├── SummaryConfig.vue │ │ ├── TaskTreeItem.vue │ │ ├── UserVideos.vue │ │ ├── VideoCategories.vue │ │ ├── VideoDetailDialog.vue │ │ ├── VideoPlayerDialog.vue │ │ ├── VideoRecord.vue │ │ ├── VideoSummary.vue │ │ ├── analytics/ │ │ │ ├── layout/ │ │ │ │ └── AnalyticsLayout.vue │ │ │ └── pages/ │ │ │ ├── AuthorCompletionPage.vue │ │ │ ├── AuthorPopularAssociationPage.vue │ │ │ ├── CategoryPopularDistributionPage.vue │ │ │ ├── DurationAnalysisPage.vue │ │ │ ├── DurationPopularDistributionPage.vue │ │ │ ├── HeroPage.vue │ │ │ ├── MonthlyPage.vue │ │ │ ├── OverallCompletionPage.vue │ │ │ ├── OverviewPage.vue │ │ │ ├── PopularHitRatePage.vue │ │ │ ├── PopularPredictionPage.vue │ │ │ ├── RewatchPage.vue │ │ │ ├── StreakPage.vue │ │ │ ├── TagsPage.vue │ │ │ ├── TimeAnalysisPage.vue │ │ │ ├── TimeDistributionPage.vue │ │ │ ├── TitleAnalysisPage.vue │ │ │ ├── TitleInteractionAnalysisPage.vue │ │ │ ├── TitleLengthAnalysisPage.vue │ │ │ ├── TitleSentimentAnalysisPage.vue │ │ │ └── TitleTrendAnalysisPage.vue │ │ ├── dynamic/ │ │ │ ├── DynamicCardNormal.vue │ │ │ └── DynamicCardVideo.vue │ │ ├── layout/ │ │ │ └── MainLayout.vue │ │ ├── page/ │ │ │ ├── AnimatedAnalytics.vue │ │ │ ├── BiliTools.vue │ │ │ ├── Comments.vue │ │ │ ├── Downloads.vue │ │ │ ├── DynamicDownloader.vue │ │ │ ├── Favorites.vue │ │ │ ├── History.vue │ │ │ ├── Images.vue │ │ │ ├── MediaManager.vue │ │ │ ├── Remarks.vue │ │ │ ├── SchedulerTasks.vue │ │ │ ├── Search.vue │ │ │ ├── VideoDetailsManager.vue │ │ │ └── VideoDownloader.vue │ │ └── scheduler/ │ │ ├── SelectDialog.vue │ │ ├── TaskDetail.vue │ │ ├── TaskForm.vue │ │ └── TaskHistory.vue │ ├── main.js │ ├── router/ │ │ └── router.js │ ├── store/ │ │ ├── darkMode.js │ │ └── privacy.js │ ├── style.css │ └── utils/ │ ├── imageProxy.js │ ├── imageUrl.js │ ├── openUrl.js │ └── privacyManager.js ├── src-tauri/ │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities/ │ │ └── default.json │ ├── icons/ │ │ └── icon.icns │ ├── src/ │ │ ├── lib.rs │ │ └── main.rs │ ├── tauri.conf.json │ ├── tauri.linux.conf.json │ ├── tauri.macos.conf.json │ └── tauri.windows.conf.json ├── tailwind.config.js └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git .gitignore node_modules Dockerfile .dockerignore dist .cache src-tauri ================================================ FILE: .github/ISSUE_TEMPLATE/问题反馈-功能请求.md ================================================ --- name: 问题反馈/功能请求 about: 提交问题反馈或新功能请求 title: '' labels: '' assignees: '' --- ## 问题类型 请在适当的选项前打 [x] - [ ] 🐛 Bug报告 - [ ] ✨ 功能请求 - [ ] 📝 文档改进 - [ ] 🎨 UI/UX改进 - [ ] 🧰 技术支持 ## 功能重要程度 请在适当的选项前打 [x] - [ ] 🔴 核心功能(影响主要使用流程) - [ ] 🟠 主要功能(影响重要使用场景) - [ ] 🟡 次要功能(改善用户体验) - [ ] 🟢 增强功能(锦上添花) ## 问题/请求详情 请尽可能详细描述您遇到的问题或需要的功能 ## 复现步骤(针对Bug) 1. 2. 3. ## 当前行为(针对Bug) 描述目前看到的结果 ## 期望行为 描述您期望看到的结果 ## 运行环境 - 操作系统: Windows 10/11, macOS, Linux等 - 浏览器: Chrome, Firefox, Safari等(附上版本号) - 应用版本: - 可执行文件(附上版本号) - 源码(附上你使用的提交hash) - 设备类型: 桌面端/移动端 ## 截图/视频 贴上相关截图或视频链接,帮助我们更好地理解问题 ## 控制台日志 如果有浏览器控制台错误,请附上相关日志 ## 额外信息 其他可能有助于解决问题的信息 ================================================ FILE: .github/workflows/docker-image.yml ================================================ name: Docker Image on: push: branches: - master tags: - "v*" workflow_dispatch: permissions: contents: read packages: write jobs: build-and-push: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/bili-history-frontend flavor: | latest=false tags: | type=ref,event=tag type=semver,pattern={{version}} type=raw,value=latest,enable={{is_default_branch}} - name: Build and push uses: docker/build-push-action@v6 with: context: . file: Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max ================================================ FILE: .github/workflows/tauri-release.yml ================================================ name: Release (Tauri) on: push: tags: - "v*" workflow_dispatch: inputs: tag: description: "Git tag to build (e.g. v1.0.0)" required: true type: string permissions: contents: write jobs: build-tauri: name: Build (${{ matrix.label }}) runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: include: - label: macos-universal platform: macos-latest args: --target universal-apple-darwin - label: linux-x64 platform: ubuntu-24.04 args: "" - label: windows-x64 platform: windows-latest args: "" steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Rust cache uses: swatinem/rust-cache@v2 with: workspaces: "./src-tauri -> target" - name: Install Linux dependencies if: matrix.platform == 'ubuntu-24.04' run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libgtk-3-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ patchelf \ libssl-dev \ pkg-config \ fakeroot \ libfuse2 - name: Install Windows dependencies if: matrix.platform == 'windows-latest' run: choco install nsis -y - name: Install frontend dependencies run: npm install --no-package-lock --include=optional --no-fund --no-audit - name: Build and upload release assets uses: tauri-apps/tauri-action@v0.6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tagName: ${{ github.event.inputs.tag || github.ref_name }} releaseName: "BiliHistoryFrontend ${{ github.event.inputs.tag || github.ref_name }}" releaseBody: "See the assets to download and install this version." releaseDraft: false prerelease: ${{ contains(github.event.inputs.tag || github.ref_name, '-') }} tauriScript: "npx tauri" args: ${{ matrix.args }} generateReleaseNotes: true ================================================ FILE: .gitignore ================================================ # generated by: https://gitignore.itranswarp.com/ #################### Node.gitignore #################### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* #################### Archives.gitignore #################### # It's better to unpack these files and commit the raw source because # git has its own built in compression methods. *.7z *.jar *.rar *.zip *.gz *.gzip *.tgz *.bzip *.bzip2 *.bz2 *.xz *.lzma *.cab *.xar # Packing-only formats *.iso *.tar # Package management formats *.dmg *.xpi *.gem *.egg *.deb *.rpm *.msi *.msm *.msp *.txz #################### Backup.gitignore #################### *.bak *.gho *.ori *.orig *.tmp #################### Emacs.gitignore #################### # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data #################### Linux.gitignore #################### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* #################### NotepadPP.gitignore #################### # Notepad++ backups # *.bak #################### PuTTY.gitignore #################### # Private key *.ppk #################### SublimeText.gitignore #################### # Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # Workspace files are user-specific *.sublime-workspace # Project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using Sublime Text # *.sublime-project # SFTP configuration file sftp-config.json sftp-config-alt*.json # Package control specific files Package Control.last-run Package Control.ca-list Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ Package Control.merged-ca-bundle Package Control.user-ca-bundle oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file # https://packagecontrol.io/packages/sublime-github GitHub.sublime-settings #################### Vim.gitignore #################### # Swap [._]*.s[a-v][a-z] !*.svg # comment out if you don't need vector files [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim Sessionx.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ #################### VisualStudioCode.gitignore #################### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix #################### Windows.gitignore #################### # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk #################### macOS.gitignore #################### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk #################### Custom.gitignore #################### # add your custom gitignore here: !.gitignore !.gitsubmodules /src-tauri/target/ /src-tauri/gen/ .idea # tauri target src-tauri/gen src-tauri/target /CLAUDE.md ================================================ FILE: Dockerfile ================================================ FROM oven/bun:1 AS base WORKDIR /app FROM base AS install COPY package.json bun.lock . RUN bun install FROM base AS build ARG BACKEND_URL_ARG=http://localhost:8899 ENV VITE_DEFAULT_BACKEND_URL=${BACKEND_URL_ARG} COPY . . COPY --from=install /app/node_modules node_modules RUN bun run build FROM caddy:2-alpine AS release COPY --from=build /app/dist /app COPY deploy/Caddyfile /etc/caddy EXPOSE 80 ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2024-present 2977094657 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Logo
这是一个基于 Vue 3 开发的 B 站历史记录分析工具的前端项目,为用户提供丰富的 B 站观看历史数据分析功能。 ## 该项目需要配合 [BilibiliHistoryFetcher](https://github.com/2977094657/BilibiliHistoryFetcher) 后端项目一起使用 ## 零基础快速运行(Windows 免安装版,推荐) 1. 下载 exe:后端 https://github.com/2977094657/BilibiliHistoryFetcher/releases/latest 2. 下载 exe:前端 https://github.com/2977094657/BiliHistoryFrontend/releases/latest 3. 两个都双击运行即可 ## 快速开始 ### 使用 Docker 安装 #### 使用预构建镜像(GitHub Container Registry) ```bash docker pull ghcr.io/2977094657/bili-history-frontend:latest docker run --name bili-history-frontend-web -p 5173:80 -d ghcr.io/2977094657/bili-history-frontend:latest ``` 1. 安装[Docker](https://docs.docker.com/get-started/get-docker/). 2. 构建镜像:`docker build -t bili-history-frontend-web:dev .` 3. 启动容器:`docker run --name bili-history-frontend-web -p 5173:80 -d bili-history-frontend-web:dev` 4. 停止容器:`docker stop bili-history-frontend-web` ### [通过 1Panel 部署](https://github.com/2977094657/BilibiliHistoryFetcher/discussions/65) 由社区贡献者 [@QYG2297248353](https://github.com/QYG2297248353) 实现 ([#66](https://github.com/2977094657/BilibiliHistoryFetcher/pull/66)) ### 使用源码安装 1. 克隆项目 ```bash git clone https://github.com/2977094657/BiliHistoryFrontend.git cd BilibiliHistoryFrontend ``` 2. 安装依赖 ```bash npm install ``` 3. 启动开发服务器 ```bash # 网页版开发 npm run dev ``` ## 首次使用指南 1. **登录账号** - 点击侧边栏的设置,然后配置你的服务器地址 - 然后点击侧边栏中的"未登录"状态 - 使用 B 站手机 APP 扫描二维码进行登录 - 登录成功后会显示你的用户名 2. **获取历史记录** - 登录成功后,点击导航栏中的"实时更新"按钮 - 首次使用时会自动获取你的全部历史记录,这可能需要一些时间 - 获取完成后数据会自动导入到本地数据库 - 页面会自动刷新并显示你的观看历史 3. **后续使用** - 默认的计划任务会在每天 0 点自动获取历史记录 - 可去设置里配置邮箱进行通知,不配置不影响自动获取,只是无法收到通知 - 每次打开页面时,建议点击"实时更新"以获取最新记录 - 实时更新只会获取新增的记录,速度很快 ## 页面介绍 **1. 年度总结页面** **2. 主页** 支持列表/网格切换与日期、分区筛选,一键实时更新,支持隐私模式。 **3. 评论** 登录后查看我的评论,支持关键词与类型筛选,并可跳转原文。 **4. 我的收藏** 支持查看我创建/收藏及本地收藏夹,可同步到本地并下载收藏内容。 **5. 媒体管理** 集中管理已下载视频与图片,查看/编辑备注与评论,并可批量补全视频详情。 **6. 计划任务** 统一管理定时与链式任务,支持新建/编辑/执行/启用或禁用,并查看历史与成功率。 **7. 设置** 配置服务器、隐私与布局、数据导出。 **8. 视频下载功能** 输入 BV/链接或 UP UID 下载单个/合集/投稿,过程实时反馈。 **9. 视频观看总时长** 查询合集级观看总时长、平均时长与完播率,可按列查看统计 **10. 动态下载** 输入用户MID下载B站动态内容,实时显示下载进度 **11. 本地摘要功能** 基于本地语音转文字结合 DeepSeek 生成视频摘要,支持模型管理、环境检测与结果缓存。 ## 使用 Tauri 构建桌面应用 ### GitHub Actions 自动构建(多平台包体) 推送 tag(例如 `v1.0.0`)后,会自动在 GitHub Releases 里生成 Windows/macOS/Linux 的安装包与产物。 **环境准备** 1. 安装 Rust 开发环境 - 按照 [Rust 官方指南](https://www.rust-lang.org/tools/install) 安装 Rust - Windows 用户还需安装 [Visual Studio C++ 构建工具](https://visualstudio.microsoft.com/visual-cpp-build-tools/) 2. 安装 Node.js 依赖 ```bash npm install ``` **开发与构建** 1. 开发模式 ```bash npm run tauri:dev ``` 这将启动一个开发服务器,并自动打开应用窗口,支持热重载。 2. 构建可执行文件 ```bash npm run tauri:build:exe ``` 构建完成后,将在项目根目录生成 `BiliBili-History-Frontend.exe` 可执行文件。 3. 清理构建文件 ```bash npm run tauri:clean ``` 清理 `src-tauri/target` 目录中的构建产物,释放磁盘空间。 ## 赞助与支持 如果本项目对你有帮助,欢迎通过以下方式赞助。付款时请在备注中填写“希望公开展示的链接”(如个人主页、B 站空间、GitHub 仓库等),我们会在 README 的“赞助鸣谢”表格中展示。
微信收款码
微信赞助
支付宝收款码
支付宝赞助
### 赞助鸣谢 | 联系内容 | 付款金额 | | ----------------------------------------------------- | -------- | | [星语半夏的个人空间-哔哩哔哩](https://b23.tv/WPHOtCS) | ¥15 | | 匿名微信用户 | ¥5 | | [EMP-NOVA13721RCL的个人空间-哔哩哔哩](https://space.bilibili.com/503193026) | ¥50 | 提示:已赞助但未收录,请在 Issues 提交凭证与备注链接;如需匿名可说明。 ## 贡献指南 欢迎提交 Issue 和 Pull Request 来帮助改进这个项目。 ## 致谢 - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) - 没有它就没有这个项目 - [Yutto](https://yutto.nyakku.moe/) - 可爱的 B 站视频下载工具 - [FasterWhisper](https://github.com/SYSTRAN/faster-whisper) - 音频转文字 - [DeepSeek](https://github.com/deepseek-ai/DeepSeek-R1) - DeepSeek AI API - [ArtPlayer](https://github.com/zhw2590582/ArtPlayer) - 强大且灵活的 HTML5 视频播放器 - [aicu.cc](https://www.aicu.cc/) - 第三方 B 站用户评论 API - [小黑盒用户 shengyI](https://www.xiaoheihe.cn/app/bbs/link/153880174) - 视频观看总时长功能思路提供者 - 所有贡献者,特别感谢: - [@eli-yip](https://github.com/eli-yip) 对 Docker 部署的贡献 - [@QYG2297248353](https://github.com/QYG2297248353) 对 1Panel 部署的贡献 ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=2977094657/BiliHistoryFrontend&type=Date)](https://star-history.com/#2977094657/BiliHistoryFrontend&Date) ================================================ FILE: deploy/Caddyfile ================================================ { auto_https off } :80 respond /health 200 { body `{"status": "ok"}` } root * /app file_server try_files {path} /index.html ================================================ FILE: index.html ================================================ b站历史记录
================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "module": "ESNext", "moduleResolution": "Node", "target": "ESNext", "jsx": "preserve", "strictFunctionTypes": false, "allowJs": true, "checkJs": false }, "exclude": ["node_modules", "dist"] } ================================================ FILE: package.json ================================================ { "name": "bilibilihistoryfrontend", "version": "1.0.0", "description": "获取Bili历史记录应用", "author": "46", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "tauri": "tauri", "tauri:dev": "tauri dev", "tauri:build": "tauri build", "tauri:build:windows": "tauri build --target x86_64-pc-windows-msvc", "tauri:build:exe": "npm run tauri:build:windows && powershell -c \"Copy-Item ./src-tauri/target/x86_64-pc-windows-msvc/release/bilibili-history-frontend.exe ./BiliBili-History-Frontend.exe -Force\"", "tauri:clean": "powershell -c \"if (Test-Path ./src-tauri/target) { Remove-Item -Recurse -Force ./src-tauri/target }\"" }, "dependencies": { "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", "@tailwindcss/forms": "^0.5.9", "@tauri-apps/plugin-shell": "~2.2.1", "@vant/touch-emulator": "^1.4.0", "@vueuse/components": "^12.3.0", "@vueuse/core": "^12.3.0", "@vueuse/motion": "^2.2.6", "animate.css": "^4.1.1", "aos": "^2.3.4", "artplayer": "^5.2.2", "artplayer-plugin-danmuku": "^5.1.5", "axios": "^1.7.7", "crypto-js": "^4.2.0", "echarts": "^5.6.0", "echarts-wordcloud": "^2.1.0", "gsap": "^3.12.5", "html2canvas": "^1.4.1", "jsencrypt": "^3.3.2", "lottie-web": "^5.12.2", "typeit": "^8.8.7", "vant": "^4.9.15", "vue": "^3.5.11", "vue-countup-v3": "^1.4.2", "vue-echarts": "^7.0.3", "vue-router": "^4.4.5", "vue3-lottie": "^3.3.1" }, "devDependencies": { "@tauri-apps/api": "~2.4.0", "@tauri-apps/cli": "~2.4.0", "@types/node": "^20.17.6", "@vitejs/plugin-vue": "^5.1.4", "@vue/runtime-core": "^3.5.13", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "postcss": "^8.4.47", "tailwindcss": "^3.4.13", "vite": "^5.4.8", "vite-plugin-vue-inspector": "^5.3.1" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/api/api.js ================================================ import axios from 'axios' // 导入通知组件 import 'vant/es/notify/style' // 你的服务器地址 const DEFAULT_FALLBACK_URL = 'http://localhost:8899'; const VITE_CONFIGURED_DEFAULT_URL = import.meta.env.VITE_DEFAULT_BACKEND_URL || DEFAULT_FALLBACK_URL; const getBaseUrl = () => { return localStorage.getItem('baseUrl') || VITE_CONFIGURED_DEFAULT_URL } const BASE_URL = getBaseUrl() // 服务器地址列表 const SERVER_URLS = [ 'http://127.0.0.1:8899', 'http://localhost:8899', 'http://0.0.0.0:8899' ] if (!SERVER_URLS.includes(VITE_CONFIGURED_DEFAULT_URL)) { SERVER_URLS.unshift(VITE_CONFIGURED_DEFAULT_URL) } // 设置服务器地址 export const setBaseUrl = (url) => { localStorage.setItem('baseUrl', url) // 更新 axios 实例的 baseURL updateInstanceBaseUrl(url) // 触发API BASE URL更新事件,供其他API模块使用 try { const event = new CustomEvent('api-baseurl-updated', { detail: { url } }) window.dispatchEvent(event) console.log('已触发API BaseURL更新事件:', url) } catch (error) { console.error('触发API BaseURL更新事件失败:', error) } window.location.reload() // 刷新页面以应用新的baseUrl } // 获取当前服务器地址 export const getCurrentBaseUrl = () => { return getBaseUrl() } // 创建一个 axios 实例 const instance = axios.create({ baseURL: BASE_URL, }) // 请求拦截器 instance.interceptors.request.use( (config) => { return config }, (error) => { return Promise.reject(error) } ) // 响应拦截器 instance.interceptors.response.use( (response) => { return response }, (error) => { return Promise.reject(error) } ) // 更新 axios 实例的 baseURL export const updateInstanceBaseUrl = (newBaseUrl) => { instance.defaults.baseURL = newBaseUrl } // 历史记录相关接口 export const getBiliHistory2024 = (page, size, sortOrder, tagName, mainCategory, dateRange, useLocalImages = false, business = '') => { return instance.get(`/history/all`, { params: { page, size, sort_order: sortOrder, tag_name: tagName, main_category: mainCategory, date_range: dateRange, use_local_images: useLocalImages, business: business, }, }) } export const searchBiliHistory2024 = (search, searchType = 'all', page = 1, size = 30, useLocalImages = false, useSessdata = true) => { return instance.get(`/history/search`, { params: { page, size, search, search_type: searchType, use_local_images: useLocalImages, use_sessdata: useSessdata }, }) } // 获取可用年份列表 export const getAvailableYears = () => { return instance.get(`/history/available-years`) } // 分类相关接口 export const getVideoCategories = () => { return instance.get(`/categories/categories`) // 使用新的分类接口 } export const getMainCategories = () => { return instance.get(`/categories/main-categories`) } // 标题分析相关接口已拆分为以下独立接口: // - getTitleKeywordAnalysis: 关键词分析 // - getTitleLengthAnalysis: 长度分析 // - getTitleSentimentAnalysis: 情感分析 // - getTitleTrendAnalysis: 趋势分析 // - getTitleInteractionAnalysis: 互动分析 // 获取标题关键词分析 export const getTitleKeywordAnalysis = (year, useCache = true) => { return instance.get(`/title/keyword-analysis`, { params: { year, use_cache: useCache } }) } // 获取标题长度分析 export const getTitleLengthAnalysis = (year, useCache = true) => { return instance.get(`/title/length-analysis`, { params: { year, use_cache: useCache } }) } // 获取标题情感分析 export const getTitleSentimentAnalysis = (year, useCache = true) => { return instance.get(`/title/sentiment-analysis`, { params: { year, use_cache: useCache } }) } // 获取标题趋势分析 export const getTitleTrendAnalysis = (year, useCache = true) => { return instance.get(`/title/trend-analysis`, { params: { year, use_cache: useCache } }) } // 获取标题互动分析 export const getTitleInteractionAnalysis = (year, useCache = true) => { return instance.get(`/title/interaction-analysis`, { params: { year, use_cache: useCache } }) } // 观看时间分析相关接口已拆分为以下独立接口: // - getViewingMonthlyStats: 月度统计分析 // - getViewingWeeklyStats: 周度统计分析 // - getViewingTimeSlots: 时段分析 // - getViewingContinuity: 观看连续性分析 // 更多维度接口将逐步添加... // 获取月度观看统计分析 export const getViewingMonthlyStats = (year, useCache = true) => { return instance.get(`/viewing/monthly-stats`, { params: { year, use_cache: useCache } }) } // 获取周度观看统计分析 export const getViewingWeeklyStats = (year, useCache = true) => { return instance.get(`/viewing/weekly-stats`, { params: { year, use_cache: useCache } }) } // 获取时段观看分析 export const getViewingTimeSlots = (year, useCache = true) => { return instance.get(`/viewing/time-slots`, { params: { year, use_cache: useCache } }) } // 获取观看连续性分析 export const getViewingContinuity = (year, useCache = true) => { return instance.get(`/viewing/continuity`, { params: { year, use_cache: useCache } }) } // 获取重复观看分析 export const getViewingWatchCounts = (year, useCache = true) => { return instance.get(`/viewing/watch-counts`, { params: { year, use_cache: useCache } }) } // 获取视频完成率分析 export const getViewingCompletionRates = (year, useCache = true) => { return instance.get(`/viewing/completion-rates`, { params: { year, use_cache: useCache } }) } // 获取UP主完成率分析 export const getViewingAuthorCompletion = (year, useCache = true) => { return instance.get(`/viewing/author-completion`, { params: { year, use_cache: useCache } }) } // 获取标签分析 export const getViewingTagAnalysis = (year, useCache = true) => { return instance.get(`/viewing/tag-analysis`, { params: { year, use_cache: useCache } }) } // 获取视频时长分析 export const getViewingDurationAnalysis = (year, useCache = true) => { return instance.get(`/viewing/duration-analysis`, { params: { year, use_cache: useCache } }) } // 原始的观看时间分析接口已删除,现在使用拆分后的独立接口 // 获取观看行为数据分析 export const getViewingBehavior = async (year, useCache = false) => { return instance.get(`/viewing/viewing/`, { params: { year, use_cache: useCache } }) } // 获取每年每天的观看数合集 export const getYearlyAnalysis = async (year) => { return instance.post(`/analysis/analyze`, null, { params: { year } }) } // 获取热门视频命中率分析 export const getPopularHitRate = async (year, useCache = true) => { return instance.get(`/popular/popular-hit-rate`, { params: { year, use_cache: useCache } }) } // 获取热门预测能力分析 export const getPopularPredictionAbility = async (year, useCache = true) => { return instance.get(`/popular/popular-prediction-ability`, { params: { year, use_cache: useCache } }) } // 获取UP主热门关联分析 export const getAuthorPopularAssociation = async (year, useCache = true) => { return instance.get(`/popular/author-popular-association`, { params: { year, use_cache: useCache } }) } // 获取热门视频分区分布分析 export const getCategoryPopularDistribution = async (year, useCache = true) => { return instance.get(`/popular/category-popular-distribution`, { params: { year, use_cache: useCache } }) } // 获取热门视频时长分布分析 export const getDurationPopularDistribution = async (year, useCache = true) => { return instance.get(`/popular/duration-popular-distribution`, { params: { year, use_cache: useCache } }) } // 实时更新历史记录 export const updateBiliHistoryRealtime = (syncDeleted = false) => { return instance.get(`/fetch/bili-history-realtime`, { params: { sync_deleted: syncDeleted } }).then(response => { // 检查响应格式 if (!response.data) { throw new Error('响应数据格式错误') } // 如果返回未找到本地历史记录错误,则调用完整获取接口 if (response.data.status === 'error' && response.data.message === '未找到本地历史记录') { return getBiliHistory() } return response }).catch(error => { console.error('API 请求错误:', error) // 重新抛出错误,让调用者处理 throw error }) } // 获取完整历史记录 export const getBiliHistory = () => { return instance.get('/fetch/bili-history').then(async response => { // 检查响应格式 if (!response.data) { throw new Error('响应数据格式错误') } // 如果获取历史记录成功,调用导入SQLite接口 if (response.data.status === 'success') { try { await importSqliteData() // 1秒后刷新页面,让用户看到成功提示 setTimeout(() => { window.location.reload() }, 1000) } catch (error) { console.error('导入SQLite失败:', error) // 即使导入失败也返回历史记录的响应 } } return response }).catch(error => { console.error('获取历史记录失败:', error) throw error }) } // 获取每日视频统计 export const getDailyStats = async (date, year) => { return instance.get(`/daily/daily-count`, { params: { date, year } }) } // 导入SQLite数据 export const importSqliteData = () => { return instance.post(`/importSqlite/import_data_sqlite`) } // 导出相关接口 // 导出历史记录到Excel export const exportHistory = (options = {}) => { // 只传递非空参数 const params = {} // 年份参数 if (options.year !== undefined && options.year !== null) { params.year = options.year } // 月份参数 if (options.month !== undefined && options.month !== null) { params.month = options.month } // 开始日期参数 if (options.start_date) { params.start_date = options.start_date } // 结束日期参数 if (options.end_date) { params.end_date = options.end_date } console.log('导出参数:', params) return instance.post('/export/export_history', null, { params }) } // 下载Excel文件 export const downloadExcelFile = (filename) => { return instance.get(`/export/download_excel/${encodeURIComponent(filename)}`, { responseType: 'blob', headers: { 'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' } }).then(response => { // 创建blob链接并下载 const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.setAttribute('download', filename) document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) return response }) } // 下载SQLite数据库 export const downloadDatabase = () => { return instance.get('/export/download_db', { responseType: 'blob', headers: { 'Accept': 'application/x-sqlite3' } }).then(response => { // 创建blob链接并下载 const blob = new Blob([response.data], { type: 'application/x-sqlite3' }) const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.setAttribute('download', 'bilibili_history.db') document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) return response }) } // 登录相关接口 // 生成登录二维码 export const generateLoginQRCode = () => { return instance.get('/login/qrcode/generate') } // 获取二维码图片URL export const getQRCodeImageURL = () => { return `${BASE_URL}/login/qrcode/image` } // 获取二维码图片(返回blob URL) export const getQRCodeImageBlob = async () => { try { const response = await instance.get('/login/qrcode/image', { responseType: 'blob' }) // 创建blob URL const blob = new Blob([response.data], { type: response.headers['content-type'] || 'image/png' }) return URL.createObjectURL(blob) } catch (error) { console.error('获取二维码图片失败:', error) throw error } } // 轮询二维码状态 export const pollQRCodeStatus = (qrcodeKey) => { return instance.get('/login/qrcode/poll', { params: { qrcode_key: qrcodeKey } }) } // 退出登录 export const logout = () => { return instance.post('/login/logout') } // 获取登录状态 export const getLoginStatus = () => { return instance.get('/login/check') } // 获取视频摘要 export const getVideoSummary = (bvid, cid, upMid, forceRefresh = false) => { return instance.get('/summary/get_summary', { params: { bvid, cid, up_mid: upMid, force_refresh: forceRefresh } }) } // 获取摘要配置 export const getSummaryConfig = () => { return instance.get('/summary/config') } // 更新摘要配置 export const updateSummaryConfig = (config) => { return instance.post('/summary/config', config) } // 批量删除历史记录 export const batchDeleteHistory = (items) => { return instance.delete('/delete/batch-delete', { data: items // 直接发送数组,不要包装在 items 对象中 }) } // 删除B站历史记录 export const deleteBilibiliHistory = (kid, syncToBilibili = true) => { return instance.delete(`/bilibili/history/single`, { params: { kid }, data: { sync_to_bilibili: syncToBilibili } }) } // 批量删除B站历史记录 export const batchDeleteBilibiliHistory = (items) => { return instance.delete('/bilibili/history/batch', { data: { items } }) } // ============================= // 热门视频数据清理(按年分库) // ============================= /** * 获取热门视频数据库可用年份(降序) * GET /bilibili/popular/years * @returns {Promise} */ export const getPopularCleanupYears = () => { return instance.get('/bilibili/popular/years') } // 数据库管理相关接口 // 重置数据库 export const resetDatabase = () => { return instance.post('/history/reset-database') } // 备注相关接口 // 更新视频备注 export const updateVideoRemark = (bvid, viewAt, remark) => { return instance.post('/history/update-remark', { bvid, view_at: viewAt, remark }) } // 批量获取视频备注 export const batchGetRemarks = (records) => { return instance.post('/history/batch-remarks', { items: records }) } // 获取所有备注记录 export const getAllRemarks = (page = 1, size = 10, sortOrder = 0) => { return instance.get('/history/remarks', { params: { page, size, sort_order: sortOrder } }) } // 获取SQLite版本 export const getSqliteVersion = () => { return instance.get('/history/sqlite-version') } // 图片管理相关接口 // 获取图片下载状态 export const getImagesStatus = () => { return instance.get('/images/status') } // 开始下载图片 export const startImagesDownload = (year = null, useSessdata = true) => { return instance.post('/images/start', null, { params: { year, use_sessdata: useSessdata } }) } // 停止下载图片 export const stopImagesDownload = () => { return instance.post('/images/stop') } // 清空图片 export const clearImages = () => { return instance.post('/images/clear') } // 下载视频 export const downloadVideo = async (bvid, sessdata = null, onMessage, downloadCover = true, onlyAudio = false, cid = 0, options = {}) => { console.log('调用下载API, bvid:', bvid, '高级选项:', options) const requestBody = { url: bvid, sessdata, download_cover: downloadCover, only_audio: onlyAudio, cid, // 添加高级选项 ...options } // 从本地存储获取API密钥 const apiKey = localStorage.getItem('apiKey') // 准备请求头 const headers = { 'Content-Type': 'application/json', } // 如果存在API密钥,添加到请求头 if (apiKey) { headers['X-API-Key'] = apiKey } const response = await fetch(`${BASE_URL}/download/download_video`, { method: 'POST', headers, body: JSON.stringify(requestBody) }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.detail || '下载请求失败') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 处理缓冲区中的完整行 const lines = buffer.split('\n') buffer = lines.pop() // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { const content = line.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } // 处理最后可能剩余的数据 if (buffer) { if (buffer.startsWith('data: ')) { const content = buffer.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } // 检查 FFmpeg 安装状态 export const checkFFmpeg = () => { return instance.get('/download/check_ffmpeg') } // 计划任务管理相关接口 export const getAllSchedulerTasks = (params = {}) => { return instance.get('/scheduler/tasks', { params }) .then(response => { return response }) .catch(error => { console.error('getAllSchedulerTasks API错误:', error) throw error }) } export const getSchedulerTaskDetail = (taskId, params = {}) => { return instance.get(`/scheduler/tasks`, { params: { task_id: taskId, include_subtasks: true, // 默认包含子任务 ...params } }).then(response => { return response; }).catch(error => { console.error('API - 获取任务详情出错:', error); throw error; }); } export const createSchedulerTask = (taskData) => { return instance.post('/scheduler/tasks', taskData) } export const updateSchedulerTask = (taskId, taskData) => { return instance.put(`/scheduler/tasks/${taskId}`, taskData) } export const deleteSchedulerTask = (taskId) => { return instance.delete(`/scheduler/tasks/${taskId}`) } export const executeSchedulerTask = (taskId, options = {}) => { return instance.post(`/scheduler/tasks/${taskId}/execute`, options) } // 子任务管理接口 export const addSubTask = (taskId, subTaskData) => { console.log('调用addSubTask API:', { taskId, subTaskData }) return instance.post(`/scheduler/tasks/${taskId}/subtasks`, subTaskData) .then(response => { console.log('addSubTask API响应:', response) return response }) .catch(error => { console.error('addSubTask API错误:', error) throw error }) } export const deleteSubTask = (taskId, subTaskId) => { return instance.delete(`/scheduler/tasks/${taskId}/subtasks/${subTaskId}`) } // 获取任务历史记录 export const getTaskHistory = ({ task_id = null, include_subtasks = true, status = null, start_date = null, end_date = null, page = 1, page_size = 20 }) => { return instance.get(`/scheduler/tasks/history`, { params: { task_id, include_subtasks, status, start_date, end_date, page, page_size } }) } // 系统接口 export const getAvailableEndpoints = () => { return instance.get('/scheduler/available-endpoints') } // 启用/禁用任务 export const setTaskEnabled = (taskId, enabled) => { return instance.post(`/scheduler/tasks/${taskId}/enable`, { enabled }) } // 邮件配置相关接口 // 获取邮件配置 export const getEmailConfig = () => { return instance.get('/config/email-config') .then(response => { console.log('邮件配置API响应成功:', response) return response }) .catch(error => { console.error('邮件配置API错误:', error) throw error }) } // 更新邮件配置 export const updateEmailConfig = (config) => { return instance.post('/config/email-config', config) .then(response => { console.log('更新邮件配置API响应成功:', response) return response }) .catch(error => { console.error('更新邮件配置API错误:', error) throw error }) } // 测试邮件配置 export const testEmailConfig = (testData) => { return instance.post('/config/test-email', testData) .then(response => { console.log('测试邮件API响应成功:', response) return response }) .catch(error => { console.error('测试邮件API错误:', error) throw error }) } // 音频转文字相关接口 export const checkAudioToTextEnvironment = () => { return instance.get('/audio_to_text/check_environment') } // 检查系统资源 export const checkSystemResources = () => { return instance.get('/audio_to_text/resource_check') } // 获取可用的 Whisper 模型列表 export const getWhisperModels = () => { return instance.get('/audio_to_text/models') } // 查找音频文件路径 export const findAudioPath = (cid) => { return instance.get('/audio_to_text/find_audio', { params: { cid } }) } // 检查语音转文字文件是否存在 export const checkSttFile = (cid) => { return instance.get('/audio_to_text/check_stt_file', { params: { cid } }) } // 转录音频文件 export const transcribeAudio = (params) => { return instance.post('/audio_to_text/transcribe', params) } // 根据CID生成视频摘要 export const summarizeByCid = (cid) => { return instance.post('/summary/summarize_by_cid', { cid }) } // 检查本地摘要文件 export const checkLocalSummary = (cid, includeContent = true) => { return instance.get(`/summary/check_local_summary/${cid}`, { params: { include_content: includeContent } }) } // 下载指定的Whisper模型 export const downloadWhisperModel = (modelSize) => { return instance.post('/audio_to_text/download_model', null, { params: { model_size: modelSize } }) } // 删除指定的Whisper模型 export const deleteWhisperModel = (modelSize) => { return instance.delete('/audio_to_text/models', { data: { model_size: modelSize } }) } // DeepSeek相关接口 export const checkDeepSeekApiKey = () => { return instance.get('/deepseek/check_api_key') } export const setDeepSeekApiKey = (apiKey) => { return instance.post('/deepseek/set_api_key', { api_key: apiKey }) } export const getDeepSeekBalance = () => { return instance.get('/deepseek/balance') } // API安全相关接口已移除 // 检查视频是否已下载 export const checkVideoDownload = (cids) => { return instance.get(`/download/check_video_download`, { params: { cids: Array.isArray(cids) ? cids.join(',') : cids } }) } // 获取已下载视频列表 export const getDownloadedVideos = (searchTerm = '', page = 1, limit = 20) => { return instance.get(`/download/list_downloaded_videos`, { params: { search_term: searchTerm, page, limit } }) } /** * 删除已下载的视频 * @param {number|null} cid 视频的CID,若为null则必须指定directory * @param {boolean} deleteDirectory 是否删除整个目录,默认为false(只删除视频文件) * @param {string|null} directory 可选,指定要删除文件的目录路径 * 若提供则在该目录中查找和删除文件 * 对于从收藏夹下载的视频,由于没有CID, * 可以设置cid为null并通过directory参数指定目录路径 * @returns {Promise} 包含删除结果信息的响应 */ export const deleteDownloadedVideo = (cid, deleteDirectory = false, directory = null) => { return instance.delete(`/download/delete_downloaded_video`, { params: { cid, delete_directory: deleteDirectory, directory: directory } }) } // 获取评论列表 export const getComments = (uid, page = 1, pageSize = 20, commentType = 'all', keyword = '', typeFilter = '') => { // 确保 typeFilter 为有效整数或不传递 const params = { page, page_size: pageSize, comment_type: commentType, keyword } // 只有当 typeFilter 有值且不为 '0' 时才添加到参数中 if (typeFilter && typeFilter !== '0') { params.type_filter = parseInt(typeFilter) } return instance.get(`/comment/query/${uid}`, { params }) } // 服务器健康检查 export const checkServerHealth = () => { return instance.get('/health') } /** * 获取视频流地址 * @param {string} file_path 视频文件路径 * @returns {string} 视频流URL */ export const getVideoStream = (file_path) => { if (!file_path) return '' const baseUrl = instance.defaults.baseURL // 构建基本URL return `${baseUrl}/download/stream_video?file_path=${encodeURIComponent(file_path)}&t=${Date.now()}` } /** * 获取弹幕文件内容 * @param {string} cid 弹幕ID * @param {string} file_path 弹幕文件路径 * @returns {Promise} 响应对象 */ export const getDanmakuFile = async (cid = '', file_path = '') => { try { const params = {}; if (cid) params.cid = cid; if (file_path) params.file_path = file_path; return await instance.get(`/download/stream_danmaku`, { params, responseType: 'text' // 获取纯文本格式的弹幕文件 }); } catch (error) { console.error('获取弹幕文件失败:', error); throw error; } } // 数据同步相关接口 /** * 数据同步API * @param {string} db_path - 数据库文件路径 * @param {string} json_path - JSON文件根目录 * @param {boolean} async_mode - 是否异步执行 * @returns {Promise} - API响应 */ export const syncData = (db_path = 'output/bilibili_history.db', json_path = 'output/history_by_date', async_mode = false) => { return instance.post('/data_sync/sync', { db_path, json_path, async_mode }) } /** * 获取最新同步结果API * @returns {Promise} - API响应 */ export const getSyncResult = () => { return instance.get('/data_sync/sync/result') } /** * 检查数据完整性API * @param {string} db_path - 数据库文件路径 * @param {string} json_path - JSON文件根目录 * @param {boolean} async_mode - 是否异步执行 * @param {boolean} force_check - 是否强制执行检查,忽略配置设置 * @returns {Promise} - API响应 */ export const checkDataIntegrity = (db_path = 'output/bilibili_history.db', json_path = 'output/history_by_date', async_mode = false, force_check = false) => { return instance.post('/data_sync/check', { db_path, json_path, async_mode, force_check }) } /** * 获取数据完整性校验配置API * @returns {Promise} - API响应 */ export const getIntegrityCheckConfig = () => { return instance.get('/data_sync/config') } /** * 获取数据完整性报告API * @returns {Promise} - API响应 */ export const getIntegrityReport = () => { return instance.get('/data_sync/report') } /** * 更新数据完整性校验配置API * @param {boolean} checkOnStartup - 是否在启动时进行数据完整性校验 * @returns {Promise} - API响应 */ export const updateIntegrityCheckConfig = (checkOnStartup) => { return instance.post('/data_sync/config', { check_on_startup: checkOnStartup }) } /** * 获取指定用户创建的所有收藏夹信息 * @param {Object} params 请求参数 * @param {number} [params.up_mid] 目标用户mid,不提供则使用当前登录用户 * @param {number} [params.type] 目标内容属性,0=全部(默认),2=视频稿件 * @param {number} [params.rid] 目标内容id,视频稿件为avid * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 收藏夹列表 */ export const getCreatedFavoriteFolders = (params = {}) => { return instance.get('/favorite/folder/created/list-all', { params }) } /** * 获取指定用户收藏的视频收藏夹列表 * @param {Object} params 请求参数 * @param {number} [params.up_mid] 目标用户mid,不提供则使用当前登录用户 * @param {number} [params.pn] 页码,默认为1 * @param {number} [params.ps] 每页项数,默认为20 * @param {string} [params.keyword] 搜索关键词 * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 收藏夹列表 */ export const getCollectedFavoriteFolders = (params = {}) => { return instance.get('/favorite/folder/collected/list', { params }) } /** * 获取收藏夹内容列表 * @param {Object} params 请求参数 * @param {number} params.media_id 目标收藏夹id(完整id) * @param {number} [params.pn] 页码,默认为1 * @param {number} [params.ps] 每页项数,默认为20 * @param {string} [params.keyword] 搜索关键词 * @param {string} [params.order] 排序方式,mtime(收藏时间,默认)或view(播放量) * @param {number} [params.type] 筛选类型,0=全部(默认),2=视频 * @param {number} [params.tid] 分区ID,0=全部(默认) * @param {string} [params.platform] 平台标识,默认为web * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 收藏夹内容列表 */ export const getFavoriteContents = (params = {}) => { return instance.get('/favorite/folder/resource/list', { params }) } /** * 获取数据库中的收藏夹列表 * @param {Object} params 请求参数 * @param {number} [params.mid] 用户UID,不提供则返回所有收藏夹 * @param {number} [params.page] 页码,默认为1 * @param {number} [params.size] 每页数量,默认为20 * @returns {Promise} 收藏夹列表 */ export const getLocalFavoriteFolders = (params = {}) => { return instance.get('/favorite/list', { params }) } /** * 获取数据库中的收藏内容列表 * @param {Object} params 请求参数 * @param {number} params.media_id 收藏夹ID * @param {number} [params.page] 页码,默认为1 * @param {number} [params.size] 每页数量,默认为20 * @returns {Promise} 内容列表 */ export const getLocalFavoriteContents = (params = {}) => { return instance.get('/favorite/content/list', { params }) } // #endregion /** * 收藏或取消收藏视频 * @param {Object} params 请求参数 * @param {number} params.rid 稿件avid (不含av前缀) * @param {string} [params.add_media_ids] 需要加入的收藏夹ID,多个用逗号分隔 * @param {string} [params.del_media_ids] 需要取消的收藏夹ID,多个用逗号分隔 * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 操作结果 */ export const favoriteResource = (params = {}) => { return instance.post('/favorite/resource/deal', params) } /** * 批量收藏或取消收藏视频 * @param {Object} params 请求参数 * @param {string} params.rids 稿件avid列表 (不含av前缀),多个用逗号分隔 * @param {string} [params.add_media_ids] 需要加入的收藏夹ID,多个用逗号分隔 * @param {string} [params.del_media_ids] 需要取消的收藏夹ID,多个用逗号分隔 * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 操作结果 */ export const batchFavoriteResource = (params = {}) => { return instance.post('/favorite/resource/batch-deal', params) } /** * 本地批量收藏或取消收藏视频 * @param {Object} params 请求参数 * @param {string} params.rids 稿件avid列表 (不含av前缀),多个用逗号分隔 * @param {string} [params.add_media_ids] 需要加入的收藏夹ID,多个用逗号分隔 * @param {string} [params.del_media_ids] 需要取消的收藏夹ID,多个用逗号分隔 * @param {string} [params.operation_type] 操作类型,`sync`=同步到远程,`local`=仅本地操作,默认为`sync` * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 操作结果 */ export const localBatchFavoriteResource = (params = {}) => { return instance.post('/favorite/resource/local-batch-deal', params) } /** * 批量检查视频收藏状态 * @param {Object} params 请求参数 * @param {Array|string} params.oids 视频av号列表,可以是数组或逗号分隔的字符串 * @param {string} [params.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @returns {Promise} 视频收藏状态 */ export const batchCheckFavoriteStatus = (params = {}) => { const requestParams = { ...params }; // 确保 oids 是数组格式 if (typeof requestParams.oids === 'string') { requestParams.oids = requestParams.oids.split(',').map(id => parseInt(id.trim(), 10)).filter(id => !isNaN(id)); } else if (!Array.isArray(requestParams.oids)) { console.error('批量检查收藏状态参数错误:oids必须是数组或逗号分隔的字符串'); requestParams.oids = []; } return instance.post('/favorite/check/batch', requestParams); } /** * 下载用户收藏夹视频 * @param {Object} options 下载选项 * @param {string} options.user_id 用户UID * @param {string} [options.fid] 收藏夹ID,不提供则下载全部收藏夹 * @param {string} [options.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @param {boolean} [options.download_cover] 是否下载封面,默认true * @param {boolean} [options.only_audio] 是否仅下载音频,默认false * @param {Function} onMessage 消息处理回调函数 * @returns {Promise} */ export const downloadFavorites = async (options, onMessage) => { console.log('调用收藏夹下载API, 参数:', options) // 提取基本选项和高级选项 const { user_id, fid, sessdata, download_cover, only_audio, ...advancedOptions } = options const requestBody = { user_id, fid, sessdata, download_cover: download_cover ?? true, only_audio: only_audio ?? false, // 添加高级选项 ...advancedOptions } // 准备请求头 const headers = { 'Content-Type': 'application/json', } const response = await fetch(`${BASE_URL}/download/download_favorites`, { method: 'POST', headers, body: JSON.stringify(requestBody) }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.detail || '下载请求失败') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 处理缓冲区中的完整行 const lines = buffer.split('\n') buffer = lines.pop() // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { const content = line.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } // 处理最后可能剩余的数据 if (buffer) { if (buffer.startsWith('data: ')) { const content = buffer.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } /** * 批量下载多个视频 * @param {Object} options 下载选项 * @param {Array} options.videos 要下载的视频列表,每个视频包含bvid、cid、title、author、cover * @param {string} [options.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @param {boolean} [options.download_cover] 是否下载封面,默认true * @param {boolean} [options.only_audio] 是否仅下载音频,默认false * @param {Function} onMessage 消息处理回调函数 * @returns {Promise} */ export const batchDownloadVideos = async (options, onMessage) => { console.log('调用批量下载API, 参数:', options) // 提取基本选项和高级选项 const { videos, sessdata, download_cover, only_audio, ...advancedOptions } = options const requestBody = { videos, sessdata, download_cover: download_cover ?? true, only_audio: only_audio ?? false, // 添加高级选项 ...advancedOptions } // 准备请求头 const headers = { 'Content-Type': 'application/json', } const response = await fetch(`${BASE_URL}/download/batch_download`, { method: 'POST', headers, body: JSON.stringify(requestBody) }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.detail || '批量下载请求失败') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 处理缓冲区中的完整行 const lines = buffer.split('\n') buffer = lines.pop() // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { const content = line.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } } /** * 下载用户全部投稿视频 * @param {Object} options 下载选项 * @param {string} options.user_id 用户UID * @param {string} [options.sessdata] 用户的SESSDATA,不提供则从配置文件读取 * @param {boolean} [options.download_cover] 是否下载封面,默认true * @param {boolean} [options.only_audio] 是否仅下载音频,默认false * @param {Function} onMessage 消息处理回调函数 * @returns {Promise} */ export const downloadUserVideos = async (options, onMessage) => { console.log('调用用户视频下载API, 参数:', options) // 提取基本选项和高级选项 const { user_id, sessdata, download_cover, only_audio, ...advancedOptions } = options const requestBody = { user_id, sessdata, download_cover: download_cover ?? true, only_audio: only_audio ?? false, // 添加高级选项 ...advancedOptions } // 准备请求头 const headers = { 'Content-Type': 'application/json', } const response = await fetch(`${BASE_URL}/download/download_user_videos`, { method: 'POST', headers, body: JSON.stringify(requestBody) }) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.detail || '下载请求失败') } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { value, done } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // 处理缓冲区中的完整行 const lines = buffer.split('\n') buffer = lines.pop() // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { const content = line.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } // 处理最后可能剩余的数据 if (buffer) { if (buffer.startsWith('data: ')) { const content = buffer.substring(6).trim() if (content && content !== 'close') { onMessage(content) } } } } /** * 获取B站视频详细信息 * @param {Object} params 参数对象 * @param {string} [params.aid] 视频的avid * @param {string} [params.bvid] 视频的bvid * @returns {Promise} 包含视频详细信息的响应 */ export const getVideoInfo = (params) => { return instance.get(`/download/video_info`, { params }) } // 获取用户投稿视频列表 export const getUserVideos = (params) => { return instance.get('/download/user_videos', { params: { mid: params.mid, pn: params.pn || 1, ps: params.ps || 30, tid: params.tid || 0, keyword: params.keyword || '', order: params.order || 'pubdate', sessdata: params.sessdata || '' } }) } /** * 批量获取视频详情(使用新的超详细接口) * @param {object} params - 请求参数 * @param {number} params.max_videos - 最多处理的视频数量,0表示全部 * @param {string} params.specific_videos - 要获取的特定视频ID列表,用逗号分隔(可选) * @param {boolean} params.use_sessdata - 是否使用SESSDATA获取详情,某些视频需要登录才能查看(可选,默认为true) * @returns {Promise} - 包含获取结果的响应 */ export const fetchVideoDetails = (params) => { let maxVideos = 100 let specificVideos = '' let useSessdata = true // 兼容旧版调用方式,同时支持对象参数和独立参数 if (typeof params === 'object') { maxVideos = params.max_videos !== undefined ? params.max_videos : 100 specificVideos = params.specific_videos || '' useSessdata = params.use_sessdata !== undefined ? params.use_sessdata : true } else if (typeof params === 'number') { // 旧版调用方式: fetchVideoDetails(maxVideos, specificVideos) maxVideos = params specificVideos = arguments[1] || '' } // 使用新的超详细视频详情接口 return instance.get('/video_details/batch_fetch_from_history', { params: { max_videos: maxVideos, specific_videos: specificVideos, use_sessdata: useSessdata } }) } /** * 创建视频详情进度的SSE连接(使用新的超详细接口) * @param {object|number} params - 请求参数或更新间隔 * @param {number} params.update_interval - 更新间隔,单位秒 * @returns {EventSource} - SSE事件源对象 */ export const createVideoDetailsProgressSSE = (params) => { let updateInterval = 0.1 // 兼容旧版调用方式,同时支持对象参数和数字参数 if (typeof params === 'object') { updateInterval = params.update_interval !== undefined ? params.update_interval : 0.1 } else if (typeof params === 'number') { updateInterval = params } const baseUrl = instance.defaults.baseURL // 构建基本URL,使用新的超详细接口 let url = `${baseUrl}/video_details/progress?update_interval=${updateInterval}` return new EventSource(url) } /** * 获取视频详情统计数据(使用新的超详细接口) * @returns {Promise} - 包含视频详情统计的响应 */ export const getVideoDetailsStats = () => { return instance.get('/video_details/stats') } /** * 获取失效视频明细 * @param {Object} params 可选过滤参数 * @returns {Promise} */ export const getInvalidVideos = (params = {}) => { return instance.get('/fetch/invalid-videos', { params }) } /** * 停止视频详情获取任务 * @returns {Promise} - 包含停止结果的响应 */ export const stopVideoDetailsFetch = () => { return instance.post('/video_details/stop') } /** * 获取视频观看时长信息(合集视频) * @param {Object} params 参数对象 * @param {string} params.bvid 视频的bvid * @param {boolean} [params.use_sessdata=true] 是否使用登录状态查询 * @returns {Promise} 包含视频合集观看时长信息的响应 */ export const getVideoSeasonInfo = (params) => { return instance.get('/download/video_season_info', { params }) } /** * 检查视频是否为合集 * @param {string} url 视频URL * @param {string} [sessdata] 可选的SESSDATA用于认证 * @returns {Promise} 包含合集信息的响应 */ export const checkCollection = (url, sessdata = null) => { const params = { url } if (sessdata) { params.sessdata = sessdata } return instance.get('/collection/check_collection', { params }) } /** * 下载整个合集 * @param {Object} params 下载参数 * @param {string} params.url 合集URL * @param {number} params.cid 视频的CID * @param {Function} onMessage 消息回调函数 * @returns {Promise} */ export const downloadCollection = async (params, onMessage) => { try { const response = await fetch(`${BASE_URL}/collection/download_collection`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params) }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const reader = response.body.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) const lines = chunk.split('\n') for (const line of lines) { if (line.startsWith('data: ')) { const message = line.slice(6) if (message && onMessage) { onMessage(message) } } } } } catch (error) { console.error('下载合集失败:', error) throw error } } // ============================= // 动态相关接口(/dynamic) // ============================= /** * 列出数据库中已有动态的 UP 列表(含名称与头像) * GET /dynamic/db/hosts * @param {number} [limit=50] - 每页数量(1-200) * @param {number} [offset=0] - 偏移量(>=0) */ export const getDynamicDbHosts = (limit = 50, offset = 0) => { return instance.get('/dynamic/db/hosts', { params: { limit, offset } }) } /** * 列出指定 UP 的动态(来自数据库) * GET /dynamic/db/space/{host_mid} * @param {string|number} hostMid - UP 的 mid * @param {number} [limit=20] - 每页数量(1-200) * @param {number} [offset=0] - 偏移量(>=0) */ export const getDynamicDbSpace = (hostMid, limit = 20, offset = 0) => { return instance.get(`/dynamic/db/space/${hostMid}`, { params: { limit, offset } }) } /** * 自动从上次位置继续抓取(页级延迟 3-5 秒,支持“页级停止”) * GET /dynamic/space/auto/{host_mid} * @param {string|number} hostMid - UP 的 mid * @param {Object} params - 查询参数 * @param {boolean} [params.need_top=false] * @param {boolean} [params.save_to_db=true] * @param {boolean} [params.save_media=true] */ export const startDynamicAutoFetch = (hostMid, params = {}) => { const { need_top = false, save_to_db = true, save_media = true } = params return instance.get(`/dynamic/space/auto/${hostMid}`, { params: { need_top, save_to_db, save_media } }) } /** * 创建动态抓取进度的 SSE 连接 * GET /dynamic/space/auto/{host_mid}/progress * @param {string|number} hostMid - UP 的 mid * @returns {EventSource} */ export const createDynamicProgressSSE = (hostMid) => { const baseUrl = instance.defaults.baseURL const url = `${baseUrl}/dynamic/space/auto/${hostMid}/progress` return new EventSource(url) } /** * 发送停止信号(当前页抓取完成后停止并记录 offset) * POST /dynamic/space/auto/{host_mid}/stop * @param {string|number} hostMid - UP 的 mid */ export const stopDynamicAutoFetch = (hostMid) => { return instance.post(`/dynamic/space/auto/${hostMid}/stop`) } // ============================= // 动态删除接口(/dynamic) // ============================= /** * 清理指定 UP 的动态及媒体文件 * DELETE /dynamic/space/{host_mid} * @param {string|number} hostMid - UP 的 mid * @returns {Promise} */ export const deleteDynamicSpace = (hostMid) => { return instance.delete(`/dynamic/space/${hostMid}`) } ================================================ FILE: src/components/PrivacyControl.vue ================================================ ================================================ FILE: src/components/tailwind/ArtPlayerWithDanmaku.vue ================================================ ================================================ FILE: src/components/tailwind/BusinessTypeSelector.vue ================================================ ================================================ FILE: src/components/tailwind/CustomDropdown.vue ================================================ ================================================ FILE: src/components/tailwind/DataSyncManager.vue ================================================ ================================================ FILE: src/components/tailwind/DownloadDialog.vue ================================================ ================================================ FILE: src/components/tailwind/EnvironmentCheck.vue ================================================ ================================================ FILE: src/components/tailwind/FavoriteDialog.vue ================================================ ================================================ FILE: src/components/tailwind/FilterDropdown.vue ================================================ ================================================ FILE: src/components/tailwind/HistoryContent.vue ================================================ ================================================ FILE: src/components/tailwind/LoginDialog.vue ================================================ ================================================ FILE: src/components/tailwind/Navbar.vue ================================================ ================================================ FILE: src/components/tailwind/Pagination.vue ================================================ ================================================ FILE: src/components/tailwind/SearchBar.vue ================================================ ================================================ FILE: src/components/tailwind/Settings.vue ================================================ ================================================ FILE: src/components/tailwind/Sidebar.vue ================================================ ================================================ FILE: src/components/tailwind/SimpleSearchBar.vue ================================================ ================================================ FILE: src/components/tailwind/SummaryConfig.vue ================================================ ================================================ FILE: src/components/tailwind/TaskTreeItem.vue ================================================ ================================================ FILE: src/components/tailwind/UserVideos.vue ================================================ ================================================ FILE: src/components/tailwind/VideoCategories.vue ================================================ ================================================ FILE: src/components/tailwind/VideoDetailDialog.vue ================================================ ================================================ FILE: src/components/tailwind/VideoPlayerDialog.vue ================================================ ================================================ FILE: src/components/tailwind/VideoRecord.vue ================================================ ================================================ FILE: src/components/tailwind/VideoSummary.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/layout/AnalyticsLayout.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/AuthorCompletionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/AuthorPopularAssociationPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/CategoryPopularDistributionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/DurationAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/DurationPopularDistributionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/HeroPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/MonthlyPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/OverallCompletionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/OverviewPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/PopularHitRatePage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/PopularPredictionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/RewatchPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/StreakPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TagsPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TimeAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TimeDistributionPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TitleAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TitleInteractionAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TitleLengthAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TitleSentimentAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/analytics/pages/TitleTrendAnalysisPage.vue ================================================ ================================================ FILE: src/components/tailwind/dynamic/DynamicCardNormal.vue ================================================ ================================================ FILE: src/components/tailwind/dynamic/DynamicCardVideo.vue ================================================ ================================================ FILE: src/components/tailwind/layout/MainLayout.vue ================================================ ================================================ FILE: src/components/tailwind/page/AnimatedAnalytics.vue ================================================ ================================================ FILE: src/components/tailwind/page/BiliTools.vue ================================================ ================================================ FILE: src/components/tailwind/page/Comments.vue ================================================ ================================================ FILE: src/components/tailwind/page/Downloads.vue ================================================ ================================================ FILE: src/components/tailwind/page/DynamicDownloader.vue ================================================ ================================================ FILE: src/components/tailwind/page/Favorites.vue ================================================ ================================================ FILE: src/components/tailwind/page/History.vue ================================================ ================================================ FILE: src/components/tailwind/page/Images.vue ================================================ ================================================ FILE: src/components/tailwind/page/MediaManager.vue ================================================ ================================================ FILE: src/components/tailwind/page/Remarks.vue ================================================ ================================================ FILE: src/components/tailwind/page/SchedulerTasks.vue ================================================ ================================================ FILE: src/components/tailwind/page/Search.vue ================================================ ================================================ FILE: src/components/tailwind/page/VideoDetailsManager.vue ================================================ ================================================ FILE: src/components/tailwind/page/VideoDownloader.vue ================================================ ================================================ FILE: src/components/tailwind/scheduler/SelectDialog.vue ================================================ ================================================ FILE: src/components/tailwind/scheduler/TaskDetail.vue ================================================ ================================================ FILE: src/components/tailwind/scheduler/TaskForm.vue ================================================ ================================================ FILE: src/components/tailwind/scheduler/TaskHistory.vue ================================================ ================================================ FILE: src/main.js ================================================ import { createApp } from 'vue' import './style.css' // Vant的库,会在桌面端自动将 mouse 事件转换成对应的 touch 事件,使得组件能够在桌面端使用 import '@vant/touch-emulator' import App from './App.vue' import { createMyRouter } from './router/router' // 使用命名导入 import Vant from 'vant' // 引入Vant组件样式 import 'vant/lib/index.css' // 初始化custom event用于API模块间通信 window.addEventListener('api-baseurl-updated', (event) => { console.log('API BaseURL 已更新:', event.detail?.url) }) async function initTauri() { try { // 尝试导入Tauri core API const { invoke } = await import('@tauri-apps/api/core') if (typeof invoke === 'function') { // 设置全局标识 window.__TAURI_INVOKE__ = invoke window.__TAURI__ = true console.log('Tauri API 初始化成功') return true } } catch (error) { return false } } ;(async function bootstrap () { // 初始化Tauri API let isTauri = await initTauri() // 根据环境创建适当的路由实例 const router = createMyRouter(isTauri ? 'hash' : 'history') const app = createApp(App) app.use(router) app.use(Vant) app.mount('#app') // 在 Tauri 环境中设置所有链接在当前窗口打开 if (isTauri) { // 重写默认的链接打开行为 document.addEventListener('click', function(e) { // 查找点击事件中是否包含链接元素 let target = e.target; while (target && target !== document) { if (target.tagName === 'A' && target.getAttribute('href')) { // 获取链接地址 const href = target.getAttribute('href'); // 如果是外部链接或绝对路径,则在当前窗口打开 if (href.startsWith('http') || href.startsWith('//') || href.startsWith('/')) { e.preventDefault(); window.location.href = href; } break; } target = target.parentNode; } }, true); } })() ================================================ FILE: src/router/router.js ================================================ import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' import History from '../components/tailwind/page/History.vue' import Search from '../components/tailwind/page/Search.vue' import AnimatedAnalytics from '../components/tailwind/page/AnimatedAnalytics.vue' import Settings from '../components/tailwind/Settings.vue' import MainLayout from '../components/tailwind/layout/MainLayout.vue' import Images from '../components/tailwind/page/Images.vue' import SchedulerTasks from '../components/tailwind/page/SchedulerTasks.vue' import Downloads from '../components/tailwind/page/Downloads.vue' import MediaManager from '../components/tailwind/page/MediaManager.vue' import Favorites from '../components/tailwind/page/Favorites.vue' import BiliTools from '../components/tailwind/page/BiliTools.vue' const routes = [ { path: '/', component: MainLayout, children: [ { path: '', component: History, }, { path: 'page/:pageNumber', component: History, }, { path: 'search/:keyword', name: 'Search', component: Search, }, { path: 'search/:keyword/page/:pageNumber', name: 'SearchPage', component: Search, }, { path: 'analytics', name: 'AnimatedAnalytics', component: AnimatedAnalytics, }, { path: 'settings', name: 'Settings', component: Settings, }, { path: 'remarks', name: 'Remarks', redirect: '/media?tab=remarks' }, { path: 'images', name: 'Images', component: Images }, { path: 'scheduler', name: 'SchedulerTasks', component: SchedulerTasks }, { path: 'downloads', name: 'Downloads', component: Downloads }, { path: 'comments', name: 'Comments', redirect: '/media?tab=comments' }, { path: 'media', name: 'MediaManager', component: MediaManager }, { path: 'about', name: 'About', redirect: '/settings?tab=about' }, { path: 'favorites', name: 'Favorites', component: Favorites }, { path: 'video-downloader', name: 'VideoDownloader', redirect: '/bili-tools?tab=video-download' }, { path: 'bili-tools', name: 'BiliTools', component: BiliTools } ] } ] // 创建路由实例的工厂函数 export const createMyRouter = (mode = 'hash') => { const history = mode === 'hash' ? createWebHashHistory() : createWebHistory() return createRouter({ history, routes, }) } ================================================ FILE: src/store/darkMode.js ================================================ import { ref, watch } from 'vue' // 深色模式状态管理 const isDarkMode = ref(false) // 从localStorage读取深色模式设置 const initDarkMode = () => { const savedMode = localStorage.getItem('darkMode') if (savedMode !== null) { isDarkMode.value = savedMode === 'true' } else { // 默认检测系统偏好 isDarkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches } applyDarkMode() } // 应用深色模式 const applyDarkMode = () => { if (isDarkMode.value) { document.documentElement.classList.add('dark') } else { document.documentElement.classList.remove('dark') } } // 切换深色模式 const toggleDarkMode = () => { isDarkMode.value = !isDarkMode.value localStorage.setItem('darkMode', isDarkMode.value.toString()) applyDarkMode() } // 监听深色模式变化 watch(isDarkMode, () => { applyDarkMode() }) export const useDarkMode = () => { return { isDarkMode, toggleDarkMode, initDarkMode } } ================================================ FILE: src/store/privacy.js ================================================ import { ref } from 'vue' // 从 localStorage 读取初始状态 const isPrivacyMode = ref(localStorage.getItem('privacyMode') === 'true') export const usePrivacyStore = () => { const togglePrivacyMode = () => { isPrivacyMode.value = !isPrivacyMode.value // 保存到 localStorage localStorage.setItem('privacyMode', isPrivacyMode.value.toString()) } const setPrivacyMode = (value) => { isPrivacyMode.value = value // 保存到 localStorage localStorage.setItem('privacyMode', value.toString()) } return { isPrivacyMode, togglePrivacyMode, setPrivacyMode } } ================================================ FILE: src/style.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* 深色模式全局样式 */ @layer base { /* 确保html和body支持深色模式 */ html { transition: background-color 0.3s, color 0.3s; } body { @apply bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100; transition: background-color 0.3s, color 0.3s; } } :root:root { --van-primary-color: #ff6699; /* 修改Vant主颜色 */ } /* 计算弹窗管理样式 */ .van-dialog { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; margin: 0 !important; max-height: 95vh !important; overflow-y: auto !important; } /* 弹窗内容滚动样式 */ .van-dialog__content { max-height: 85vh; overflow-y: auto; } ================================================ FILE: src/utils/imageProxy.js ================================================ /** * 图片代理服务 * 使用公共代理服务获取跨域图片 */ /** * 从原始URL获取代理图片URL * @param {string} originalUrl 原始图片URL * @returns {string} 处理后的URL */ export const getImageProxyUrl = (originalUrl) => { // 检查是否需要代理 if (!originalUrl) return originalUrl; // 数据URL和blob URL不需要代理 if (originalUrl.startsWith('data:') || originalUrl.startsWith('blob:')) { return originalUrl; } // 相对URL不需要代理 if (!/^https?:\/\/|^\/\//.test(originalUrl)) { return originalUrl; } // 使用images.weserv.nl (较稳定的公共代理) return `https://images.weserv.nl/?url=${encodeURIComponent(originalUrl)}`; }; export default { getImageProxyUrl }; ================================================ FILE: src/utils/imageUrl.js ================================================ // 规范化图片 URL: // - data:/blob: 保持不变 // - // 开头的协议相对地址 -> https: // - http/https 绝对地址 -> 保持不变 // - 以 / 开头或相对路径 -> 拼接当前 API BaseURL import { getCurrentBaseUrl } from '@/api/api' export const normalizeImageUrl = (url) => { if (!url) return '' if (typeof url !== 'string') return url // 数据或 blob URL if (url.startsWith('data:') || url.startsWith('blob:')) return url // 协议相对 URL if (url.startsWith('//')) return `https:${url}` // 已是绝对 URL if (/^https?:\/\//i.test(url)) return url // 其余按相对路径处理,拼接当前 API BaseURL const base = getCurrentBaseUrl && getCurrentBaseUrl() if (!base) return url const baseNormalized = String(base).replace(/\/$/, '') const pathNormalized = url.startsWith('/') ? url : `/${url}` return `${baseNormalized}${pathNormalized}` } /** * 将相对 output 路径转换为可访问的静态 URL(自动拼接 /static/ 前缀,并处理反斜杠) * 例如:"dynamic\\341376543\\face.jpg" -> "/static/dynamic/341376543/face.jpg" * 若已是 http(s)/data/blob,则保持不变 * 若已以 /static/ 开头,则直接规范化为完整 URL */ export const toStaticUrl = (relativePath) => { if (!relativePath) return '' if (typeof relativePath !== 'string') return relativePath // 已是绝对/数据/Blob URL if (relativePath.startsWith('data:') || relativePath.startsWith('blob:') || /^https?:\/\//i.test(relativePath)) { return relativePath } // 统一分隔符 const normalized = relativePath.replace(/\\/g, '/').replace(/^\/+/, '') // 已包含 /static/ 前缀 const staticPath = normalized.startsWith('static/') || normalized.startsWith('/static/') ? (normalized.startsWith('/') ? normalized : `/${normalized}`) : `/static/${normalized}` // 拼接域名 return normalizeImageUrl(staticPath) } export default { normalizeImageUrl, toStaticUrl, } ================================================ FILE: src/utils/openUrl.js ================================================ /** * 在系统默认浏览器中打开URL * 在Tauri环境中使用shell API,在浏览器环境中使用window.open * @param {string} url - 要打开的URL */ export async function openInBrowser(url) { // 检测Tauri环境 const hasTauriInvoke = window && typeof window.__TAURI_INVOKE__ === 'function' const hasTauriGlobal = window && window.__TAURI__ const userAgent = navigator.userAgent const isTauriUserAgent = userAgent.includes('Tauri') const isLocalhost = window.location.hostname === 'localhost' const isTauriPort = window.location.port === '1420' // Tauri默认端口 const hasFileProtocol = window.location.protocol === 'tauri:' // 检测是否应该尝试Tauri API const shouldTryTauri = hasTauriInvoke || hasTauriGlobal || isTauriUserAgent || isTauriPort || hasFileProtocol if (shouldTryTauri) { // 方法1: 直接使用__TAURI_INVOKE__ if (hasTauriInvoke) { try { await window.__TAURI_INVOKE__('plugin:shell|open', { path: url }) return } catch (error) { console.warn('Tauri __TAURI_INVOKE__ 失败:', error.message) } } // 方法2: 尝试导入@tauri-apps/api/core try { const { invoke } = await import('@tauri-apps/api/core') if (typeof invoke === 'function') { await invoke('plugin:shell|open', { path: url }) return } } catch (error) { console.warn('Tauri core API 失败:', error.message) } // 方法3: 尝试导入@tauri-apps/plugin-shell try { const { open } = await import('@tauri-apps/plugin-shell') if (typeof open === 'function') { await open(url) return } } catch (error) { console.warn('Tauri shell plugin 失败:', error.message) } } // 回退到window.open window.open(url, '_blank') } ================================================ FILE: src/utils/privacyManager.js ================================================ /** * 隐私模式管理工具 * 集中管理隐私模式状态,提供全局API */ // 隐私模式事件名称 const PRIVACY_MODE_EVENT = 'privacy-mode-changed' /** * 检查隐私模式是否启用 * @returns {boolean} 隐私模式是否启用 */ export const isPrivacyModeEnabled = () => { return localStorage.getItem('privacyMode') === 'true' } /** * 启用隐私模式 */ export const enablePrivacyMode = () => { localStorage.setItem('privacyMode', 'true') // 触发隐私模式变更事件 dispatchPrivacyModeChanged(true) console.log('隐私模式已启用') } /** * 禁用隐私模式 */ export const disablePrivacyMode = () => { localStorage.setItem('privacyMode', 'false') // 触发隐私模式变更事件 dispatchPrivacyModeChanged(false) console.log('隐私模式已禁用') } /** * 切换隐私模式 * @returns {boolean} 切换后的状态 */ export const togglePrivacyMode = () => { const currentState = isPrivacyModeEnabled() const newState = !currentState if (newState) { enablePrivacyMode() } else { disablePrivacyMode() } return newState } /** * 分发隐私模式变更事件 * @param {boolean} enabled 隐私模式是否启用 */ export const dispatchPrivacyModeChanged = (enabled) => { window.dispatchEvent(new CustomEvent(PRIVACY_MODE_EVENT, { detail: { enabled } })) } /** * 添加隐私模式变更监听器 * @param {Function} callback 回调函数 */ export const addPrivacyModeListener = (callback) => { window.addEventListener(PRIVACY_MODE_EVENT, (event) => { if (event.detail && typeof event.detail.enabled === 'boolean') { callback(event.detail.enabled) } }) } export default { isEnabled: isPrivacyModeEnabled, enable: enablePrivacyMode, disable: disablePrivacyMode, toggle: togglePrivacyMode, addListener: addPrivacyModeListener } ================================================ FILE: src-tauri/Cargo.toml ================================================ [package] name = "bilibili-history-frontend" version = "1.0.0" description = "获取Bili历史记录应用" authors = ["46"] license = "" repository = "" edition = "2021" rust-version = "1.77.2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "~2.1.0", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "~2.4.0", features = [] } tauri-plugin-log = "2.0.0" tauri-plugin-shell = "~2.2.1" ================================================ FILE: src-tauri/build.rs ================================================ fn main() { tauri_build::build() } ================================================ FILE: src-tauri/capabilities/default.json ================================================ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", "windows": [ "main" ], "permissions": [ "core:default", "shell:allow-open" ] } ================================================ FILE: src-tauri/src/lib.rs ================================================ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) .build(), )?; } Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ================================================ FILE: src-tauri/src/main.rs ================================================ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { app_lib::run(); } ================================================ FILE: src-tauri/tauri.conf.json ================================================ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "BiliBili History Frontend", "version": "1.0.0", "identifier": "com.ab46.history", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:5173", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" }, "app": { "windows": [ { "title": "BiliBili History Frontend", "width": 1024, "height": 768, "resizable": true, "fullscreen": false, "center": true, "devtools": true } ], "security": { "csp": null } }, "plugins": { "shell": { "open": true } }, "bundle": { "active": true, "targets": [], "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ] } } ================================================ FILE: src-tauri/tauri.linux.conf.json ================================================ { "bundle": { "targets": ["deb", "appimage"] } } ================================================ FILE: src-tauri/tauri.macos.conf.json ================================================ { "bundle": { "targets": ["dmg"] } } ================================================ FILE: src-tauri/tauri.windows.conf.json ================================================ { "bundle": { "targets": ["nsis"] } } ================================================ FILE: tailwind.config.js ================================================ import forms from '@tailwindcss/forms' /** @type {import('tailwindcss').Config} */ export default { darkMode: 'class', // 启用 class 模式的深色模式 content: [ './src/components/tailwind/*.vue', './src/components/tailwind/**/*.vue', './src/App.vue', // 其他文件... ], theme: { extend: { colors: { bg: '#F1F2F3', }, }, screens: { ssm: '0px', lm: { max: '640px' }, ld: { max: '768px' }, llg: { max: '1023px' }, sm: '640px', smd: '641px', md: '768px', lg: '1024px', xl: '1280px', '2xl': '1536px', }, }, plugins: [forms], } ================================================ FILE: vite.config.js ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import Inspector from 'vite-plugin-vue-inspector' import path from 'path' // 获取环境变量 const isElectron = process.env.ELECTRON === 'true' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ Inspector({ toggleComboKey: 'alt-x', }), vue(), ], base: isElectron ? './' : '/', // Electron 使用相对路径,Web 使用绝对路径 build: { emptyOutDir: true, }, server: { host: '0.0.0.0', }, resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, })