├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── 通常问题.md ├── release.md └── workflows │ ├── beta-build.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vercelignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── apps ├── cli │ ├── LICENSE │ ├── README.md │ ├── build.config.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── ConfigManager.ts │ │ │ └── FetchManager.ts │ │ └── utils │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ └── progress.ts │ └── tsconfig.json ├── monkey │ ├── .env │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── component │ │ │ ├── DateRange.vue │ │ │ ├── Header.vue │ │ │ ├── LazyImage.vue │ │ │ ├── Logo.vue │ │ │ ├── Setting.vue │ │ │ ├── configs │ │ │ │ ├── FetchStats.vue │ │ │ │ ├── Options.vue │ │ │ │ ├── SearchUser.vue │ │ │ │ └── StartButton.vue │ │ │ └── icon │ │ │ │ └── Github.vue │ │ ├── composables │ │ │ ├── useConfig.ts │ │ │ ├── useFetch.ts │ │ │ └── usePost.ts │ │ ├── main.ts │ │ ├── style.css │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── web-v2 │ ├── .gitignore │ ├── .storybook │ │ ├── main.js │ │ ├── preview.js │ │ └── vitest.setup.ts │ ├── README.md │ ├── components.json │ ├── docs │ │ ├── .vitepress │ │ │ ├── components │ │ │ │ ├── Giscus.vue │ │ │ │ └── Layout.vue │ │ │ ├── config.ts │ │ │ └── theme │ │ │ │ └── index.ts │ │ ├── cli.md │ │ ├── faq.md │ │ ├── index.md │ │ ├── intro.md │ │ ├── monkey.md │ │ ├── roadmap.md │ │ ├── server.md │ │ └── web.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── emoji.json │ │ ├── icon.webp │ │ ├── placeholder.webp │ │ └── robots.txt │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── AppSidebar.vue │ │ │ ├── CalendarView.vue │ │ │ ├── DatePicker.vue │ │ │ ├── EmptyWeibo.vue │ │ │ ├── Landing.tsx │ │ │ ├── SearchBar.vue │ │ │ ├── SwitchUser.vue │ │ │ ├── album │ │ │ │ ├── AlbumPhotos.vue │ │ │ │ ├── AlbumPreview.vue │ │ │ │ └── AlbumPreviewWeibo.vue │ │ │ ├── common │ │ │ │ ├── Avatar.vue │ │ │ │ ├── BackToTop.vue │ │ │ │ ├── DateRange.vue │ │ │ │ ├── DateSelect.vue │ │ │ │ ├── ImageGallery.vue │ │ │ │ ├── ImagePreview.vue │ │ │ │ ├── ImportData.vue │ │ │ │ ├── ImportDataPreview.tsx │ │ │ │ ├── LazyImage.vue │ │ │ │ ├── OpenSetting.vue │ │ │ │ ├── Pagination.vue │ │ │ │ └── SearchBar.vue │ │ │ ├── followings │ │ │ │ ├── DataTable.vue │ │ │ │ ├── DataTableDropdown.vue │ │ │ │ ├── DataTableHeader.vue │ │ │ │ ├── DataTablePagination.vue │ │ │ │ ├── FollowingsTable.vue │ │ │ │ └── dataColumns.tsx │ │ │ ├── settings │ │ │ │ ├── AboutSettings.vue │ │ │ │ ├── GeneralSettings.vue │ │ │ │ ├── ImageSourceOption.vue │ │ │ │ ├── Settings.vue │ │ │ │ ├── SettingsHeader.vue │ │ │ │ └── SettingsSidebar.vue │ │ │ ├── ui │ │ │ │ ├── accordion │ │ │ │ │ ├── Accordion.vue │ │ │ │ │ ├── AccordionContent.vue │ │ │ │ │ ├── AccordionItem.vue │ │ │ │ │ ├── AccordionTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── avatar │ │ │ │ │ ├── Avatar.vue │ │ │ │ │ ├── AvatarFallback.vue │ │ │ │ │ ├── AvatarImage.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── badge │ │ │ │ │ ├── Badge.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── button │ │ │ │ │ ├── Button.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── calendar │ │ │ │ │ ├── Calendar.vue │ │ │ │ │ ├── CalendarCell.vue │ │ │ │ │ ├── CalendarCellTrigger.vue │ │ │ │ │ ├── CalendarGrid.vue │ │ │ │ │ ├── CalendarGridBody.vue │ │ │ │ │ ├── CalendarGridHead.vue │ │ │ │ │ ├── CalendarGridRow.vue │ │ │ │ │ ├── CalendarHeadCell.vue │ │ │ │ │ ├── CalendarHeader.vue │ │ │ │ │ ├── CalendarHeading.vue │ │ │ │ │ ├── CalendarNextButton.vue │ │ │ │ │ ├── CalendarPrevButton.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── card │ │ │ │ │ ├── Card.vue │ │ │ │ │ ├── CardAction.vue │ │ │ │ │ ├── CardContent.vue │ │ │ │ │ ├── CardDescription.vue │ │ │ │ │ ├── CardFooter.vue │ │ │ │ │ ├── CardHeader.vue │ │ │ │ │ ├── CardTitle.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── collapsible │ │ │ │ │ ├── Collapsible.vue │ │ │ │ │ ├── CollapsibleContent.vue │ │ │ │ │ ├── CollapsibleTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── dialog │ │ │ │ │ ├── Dialog.vue │ │ │ │ │ ├── DialogClose.vue │ │ │ │ │ ├── DialogContent.vue │ │ │ │ │ ├── DialogDescription.vue │ │ │ │ │ ├── DialogFooter.vue │ │ │ │ │ ├── DialogHeader.vue │ │ │ │ │ ├── DialogOverlay.vue │ │ │ │ │ ├── DialogProvider.vue │ │ │ │ │ ├── DialogScrollContent.vue │ │ │ │ │ ├── DialogTitle.vue │ │ │ │ │ ├── DialogTrigger.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useDialog.ts │ │ │ │ ├── dropdown-menu │ │ │ │ │ ├── DropdownMenu.vue │ │ │ │ │ ├── DropdownMenuCheckboxItem.vue │ │ │ │ │ ├── DropdownMenuContent.vue │ │ │ │ │ ├── DropdownMenuGroup.vue │ │ │ │ │ ├── DropdownMenuItem.vue │ │ │ │ │ ├── DropdownMenuLabel.vue │ │ │ │ │ ├── DropdownMenuRadioGroup.vue │ │ │ │ │ ├── DropdownMenuRadioItem.vue │ │ │ │ │ ├── DropdownMenuSeparator.vue │ │ │ │ │ ├── DropdownMenuShortcut.vue │ │ │ │ │ ├── DropdownMenuSub.vue │ │ │ │ │ ├── DropdownMenuSubContent.vue │ │ │ │ │ ├── DropdownMenuSubTrigger.vue │ │ │ │ │ ├── DropdownMenuTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── input │ │ │ │ │ ├── Input.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── label │ │ │ │ │ ├── Label.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── popover │ │ │ │ │ ├── Popover.vue │ │ │ │ │ ├── PopoverAnchor.vue │ │ │ │ │ ├── PopoverContent.vue │ │ │ │ │ ├── PopoverTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── radio-group │ │ │ │ │ ├── RadioGroup.vue │ │ │ │ │ ├── RadioGroupItem.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── scroll-area │ │ │ │ │ ├── ScrollArea.vue │ │ │ │ │ ├── ScrollBar.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── select │ │ │ │ │ ├── Select.vue │ │ │ │ │ ├── SelectContent.vue │ │ │ │ │ ├── SelectGroup.vue │ │ │ │ │ ├── SelectItem.vue │ │ │ │ │ ├── SelectItemText.vue │ │ │ │ │ ├── SelectLabel.vue │ │ │ │ │ ├── SelectScrollDownButton.vue │ │ │ │ │ ├── SelectScrollUpButton.vue │ │ │ │ │ ├── SelectSeparator.vue │ │ │ │ │ ├── SelectTrigger.vue │ │ │ │ │ ├── SelectValue.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── separator │ │ │ │ │ ├── Separator.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── sheet │ │ │ │ │ ├── Sheet.vue │ │ │ │ │ ├── SheetClose.vue │ │ │ │ │ ├── SheetContent.vue │ │ │ │ │ ├── SheetDescription.vue │ │ │ │ │ ├── SheetFooter.vue │ │ │ │ │ ├── SheetHeader.vue │ │ │ │ │ ├── SheetOverlay.vue │ │ │ │ │ ├── SheetTitle.vue │ │ │ │ │ ├── SheetTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── sidebar │ │ │ │ │ ├── Sidebar.vue │ │ │ │ │ ├── SidebarContent.vue │ │ │ │ │ ├── SidebarFooter.vue │ │ │ │ │ ├── SidebarGroup.vue │ │ │ │ │ ├── SidebarGroupAction.vue │ │ │ │ │ ├── SidebarGroupContent.vue │ │ │ │ │ ├── SidebarGroupLabel.vue │ │ │ │ │ ├── SidebarHeader.vue │ │ │ │ │ ├── SidebarInput.vue │ │ │ │ │ ├── SidebarInset.vue │ │ │ │ │ ├── SidebarMenu.vue │ │ │ │ │ ├── SidebarMenuAction.vue │ │ │ │ │ ├── SidebarMenuBadge.vue │ │ │ │ │ ├── SidebarMenuButton.vue │ │ │ │ │ ├── SidebarMenuButtonChild.vue │ │ │ │ │ ├── SidebarMenuItem.vue │ │ │ │ │ ├── SidebarMenuSkeleton.vue │ │ │ │ │ ├── SidebarMenuSub.vue │ │ │ │ │ ├── SidebarMenuSubButton.vue │ │ │ │ │ ├── SidebarMenuSubItem.vue │ │ │ │ │ ├── SidebarProvider.vue │ │ │ │ │ ├── SidebarRail.vue │ │ │ │ │ ├── SidebarSeparator.vue │ │ │ │ │ ├── SidebarTrigger.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── skeleton │ │ │ │ │ ├── Skeleton.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── switch │ │ │ │ │ ├── Switch.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── table │ │ │ │ │ ├── Table.vue │ │ │ │ │ ├── TableBody.vue │ │ │ │ │ ├── TableCaption.vue │ │ │ │ │ ├── TableCell.vue │ │ │ │ │ ├── TableEmpty.vue │ │ │ │ │ ├── TableFooter.vue │ │ │ │ │ ├── TableHead.vue │ │ │ │ │ ├── TableHeader.vue │ │ │ │ │ ├── TableRow.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── tabs │ │ │ │ │ ├── Tabs.vue │ │ │ │ │ ├── TabsContent.vue │ │ │ │ │ ├── TabsList.vue │ │ │ │ │ ├── TabsTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ └── tooltip │ │ │ │ │ ├── Tooltip.vue │ │ │ │ │ ├── TooltipContent.vue │ │ │ │ │ ├── TooltipProvider.vue │ │ │ │ │ ├── TooltipTrigger.vue │ │ │ │ │ └── index.ts │ │ │ └── weibo │ │ │ │ ├── Weibo.vue │ │ │ │ ├── WeiboActions.vue │ │ │ │ ├── WeiboCard.vue │ │ │ │ ├── WeiboComments.vue │ │ │ │ ├── WeiboEmoji.vue │ │ │ │ ├── WeiboLinkCard.vue │ │ │ │ ├── WeiboProfile.vue │ │ │ │ ├── WeiboRetweet.vue │ │ │ │ └── WeiboText.tsx │ │ ├── composables │ │ │ ├── index.ts │ │ │ └── useSearch.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.ts │ │ ├── pages │ │ │ ├── album.vue │ │ │ ├── bookmarks.vue │ │ │ ├── followings.vue │ │ │ ├── index.vue │ │ │ ├── memos.vue │ │ │ ├── post.vue │ │ │ └── search.vue │ │ ├── routes │ │ │ └── index.ts │ │ ├── stores │ │ │ ├── index.ts │ │ │ ├── postStore.ts │ │ │ └── userStore.ts │ │ ├── stories │ │ │ ├── Album.stories.ts │ │ │ ├── ImageGallery.stories.ts │ │ │ ├── ImagePreview.stories.ts │ │ │ ├── ImportDataPreview.stories.ts │ │ │ ├── LazyImage.stories.ts │ │ │ ├── Pagination.stories.ts │ │ │ ├── Settings.stories.ts │ │ │ ├── SwitchUser.stories.ts │ │ │ ├── Weibo.stories.ts │ │ │ ├── components │ │ │ │ ├── RouterLink.vue │ │ │ │ └── index.ts │ │ │ ├── shadcn │ │ │ │ └── button.stories.ts │ │ │ └── test.data.ts │ │ ├── style.css │ │ └── types.ts │ ├── tsconfig.json │ ├── vercel-build.js │ ├── vercel.json │ ├── vite.config.ts │ └── vitest.workspace.ts └── web │ └── vercel-build.js ├── eslint.config.js ├── package.json ├── packages └── core │ ├── README.md │ ├── package.json │ └── src │ ├── constants │ └── index.ts │ ├── index.ts │ ├── services │ ├── fetchService.ts │ ├── index.ts │ ├── parseService.ts │ ├── postService.ts │ └── userService.ts │ ├── types │ ├── fetchArgs.ts │ ├── index.ts │ ├── post.ts │ ├── raw │ │ ├── favorites │ │ │ └── all_fav.ts │ │ ├── friendships │ │ │ └── friends.ts │ │ ├── index.ts │ │ ├── profile │ │ │ ├── detail.ts │ │ │ ├── followContent.ts │ │ │ └── info.ts │ │ ├── side │ │ │ └── search.ts │ │ └── statuses │ │ │ ├── buildComments.ts │ │ │ ├── longtext.ts │ │ │ └── mymblog.ts │ └── user.ts │ └── utils │ ├── IndexedDB.ts │ ├── dom.ts │ ├── emitter.ts │ ├── error.ts │ ├── export.ts │ ├── fetch.ts │ ├── format.ts │ ├── index.ts │ └── pqueue.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── server ├── README.md ├── build.sh ├── cmd │ └── main.go ├── go.mod ├── go.sum └── internal │ ├── config │ └── config.go │ ├── server │ ├── images.go │ ├── server.go │ └── static.go │ ├── ui │ ├── config.go │ ├── interactive.go │ ├── model.go │ ├── renderer.go │ ├── update.go │ └── validator.go │ └── utils │ ├── argv.go │ ├── comom.go │ ├── downloader.go │ ├── files.go │ └── queue.go ├── tsconfig.json └── vercel.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | description: 运行时遇到的 bug 3 | labels: Bug 4 | body: 5 | - type: textarea 6 | id: bug-description 7 | attributes: 8 | label: Bug 描述 9 | description: 请描述遇到的 bug 10 | placeholder: 请输入 11 | - type: textarea 12 | id: reproduce 13 | attributes: 14 | label: 复现步骤 15 | description: 请描述如何复现这个 bug 16 | placeholder: 请输入 17 | - type: textarea 18 | id: script-version 19 | attributes: 20 | label: 脚本版本 21 | description: 请提供脚本版本号 22 | placeholder: 请输入 23 | - type: textarea 24 | id: browser-version 25 | attributes: 26 | label: 浏览器版本 27 | description: 请提供浏览器版本号 28 | placeholder: 请输入 29 | - type: textarea 30 | id: logs 31 | attributes: 32 | label: 错误信息 33 | description: 下载图片、启动服务时报告的错误信息, 或者浏览器开发者工具 (F12 或 Ctrl+Shift+I 召唤) 里 Console / 控制台 一栏的输出, 太长的话可以截图放下面. 34 | placeholder: 请输入 35 | - type: textarea 36 | id: screenshots 37 | attributes: 38 | label: 附加截图 39 | placeholder: 可在此粘贴图片 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 转到讨论区 4 | url: https://github.com/Chilfish/Weibo-archiver/discussions/ 5 | about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/通常问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 通常问题 3 | about: 没有格式限制的 issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/release.md: -------------------------------------------------------------------------------- 1 | 查看更多更新日志:[CHANGELOG.md](https://github.com/Chilfish/Weibo-archiver/blob/main/CHANGELOG.md) 2 | [使用教程](https://docs.qq.com/doc/DTWttbXlMUGxZZnZq) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | *.local 5 | dist 6 | dist-ssr 7 | node_modules 8 | .idea/ 9 | *.log 10 | .env 11 | *.zip 12 | 13 | apps/web/public/assets/img/ 14 | packages/core/src/constants/data.mjs 15 | 16 | .vercel 17 | 18 | tmp/ 19 | 20 | *.exe 21 | *.dmg 22 | *.deb 23 | *.rpm 24 | *.tar.gz 25 | *.tar.xz 26 | *.tar.bz2 27 | 28 | auto-components.d.ts 29 | 30 | cache/ 31 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true 4 | dedupe-peer-dependents=false 5 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | .vscode 4 | dist 5 | node_modules 6 | apps/desktop 7 | apps/monkey 8 | apps/cli 9 | packages/database 10 | scripts 11 | CHANGELOG.md 12 | README.md 13 | 14 | *.log 15 | .output 16 | .nuxt 17 | .idea 18 | .vercel 19 | eslint.config.ts 20 | LICENSE 21 | .editorconfig 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.vite", 4 | "antfu.iconify", 5 | "antfu.unocss", 6 | "antfu.goto-alias", 7 | "vue.volar", 8 | "dbaeumer.vscode-eslint", 9 | "EditorConfig.EditorConfig" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.css": "tailwindcss" 4 | }, 5 | 6 | // Enable the ESlint flat config support 7 | "eslint.experimental.useFlatConfig": true, 8 | 9 | // Disable the default formatter 10 | "prettier.enable": false, 11 | "editor.formatOnSave": false, 12 | 13 | // Auto fix 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit", 16 | "source.organizeImports": "never" 17 | }, 18 | 19 | // Silent the stylistic rules in you IDE, but still auto fix them 20 | "eslint.rules.customizations": [ 21 | { "rule": "style/*", "severity": "off" }, 22 | { "rule": "*-indent", "severity": "off" }, 23 | { "rule": "*-spacing", "severity": "off" }, 24 | { "rule": "*-spaces", "severity": "off" }, 25 | { "rule": "*-order", "severity": "off" }, 26 | { "rule": "*-dangle", "severity": "off" }, 27 | { "rule": "*-newline", "severity": "off" }, 28 | { "rule": "*quotes", "severity": "off" }, 29 | { "rule": "*semi", "severity": "off" } 30 | ], 31 | 32 | // The following is optional. 33 | // It's better to put under project setting `.vscode/settings.json` 34 | // to avoid conflicts with working with different eslint configs 35 | // that does not support all formats. 36 | "eslint.validate": [ 37 | "javascript", 38 | "javascriptreact", 39 | "typescript", 40 | "typescriptreact", 41 | "vue", 42 | "html", 43 | "markdown", 44 | "json", 45 | "jsonc", 46 | "yaml" 47 | ], 48 | "typescript.tsdk": "node_modules\\typescript\\lib" 49 | } 50 | -------------------------------------------------------------------------------- /apps/cli/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [{ 5 | input: 'src/index.ts', 6 | name: 'weibo-archiver', 7 | }], 8 | declaration: false, 9 | clean: true, 10 | failOnWarn: false, 11 | rollup: { 12 | emitCJS: false, 13 | esbuild: { 14 | target: 'esnext', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /apps/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weibo-archiver", 3 | "type": "module", 4 | "version": "0.6.0", 5 | "description": "Weibo archiver CLI tool", 6 | "author": { 7 | "name": "Chilfish", 8 | "email": "chil4fish@gmail.com" 9 | }, 10 | "license": "Apache-2.0", 11 | "homepage": "https://github.com/Chilfish/Weibo-archiver", 12 | "bugs": { 13 | "url": "https://github.com/Chilfish/Weibo-archiver/issues" 14 | }, 15 | "module": "./dist/weibo-archiver.mjs", 16 | "bin": { 17 | "weibo-archiver": "./dist/weibo-archiver.mjs" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "engines": { 23 | "node": ">=20.0.0" 24 | }, 25 | "scripts": { 26 | "build": "unbuild", 27 | "dev": "unbuild --stub", 28 | "lint": "eslint .", 29 | "start": "node weibo-archiver.mjs" 30 | }, 31 | "devDependencies": { 32 | "@types/yargs": "^17.0.33", 33 | "@weibo-archiver/core": "workspace:^", 34 | "yargs": "^17.7.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/cli/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file' 2 | export * from './progress' 3 | -------------------------------------------------------------------------------- /apps/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "Node", 9 | "paths": { 10 | "@weibo-archiver/core": ["packages/core"] 11 | }, 12 | "allowJs": true, 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "noUnusedLocals": true, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/monkey/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_VERSION=$npm_package_version 2 | -------------------------------------------------------------------------------- /apps/monkey/.gitignore: -------------------------------------------------------------------------------- 1 | !.env 2 | -------------------------------------------------------------------------------- /apps/monkey/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@weibo-archiver/monkey", 3 | "type": "module", 4 | "version": "0.6.0", 5 | "private": true, 6 | "author": "Chilfish", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "vite --port=3333", 10 | "build": "vite build" 11 | }, 12 | "dependencies": { 13 | "@weibo-archiver/core": "workspace:^" 14 | }, 15 | "devDependencies": { 16 | "@tailwindcss/vite": "^4.1.7", 17 | "@types/file-saver": "^2.0.7", 18 | "@vitejs/plugin-vue": "^5.2.4", 19 | "daisyui": "^5.0.35", 20 | "tailwindcss": "^4.1.7", 21 | "vite": "^6.3.5", 22 | "vite-plugin-monkey": "^5.0.8" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/monkey/src/component/Header.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 17 | 18 | 21 | 24 | Weibo archiver 25 | 26 | 31 | 在预览网站导入数据 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /apps/monkey/src/component/LazyImage.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/monkey/src/component/Logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /apps/monkey/src/component/configs/StartButton.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 37 | 41 | 45 | 49 | {{ buttonText }} 50 | 51 | 52 | 56 | 直接导出缓存数据 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /apps/monkey/src/component/icon/Github.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/monkey/src/composables/useConfig.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@weibo-archiver/core' 2 | import { useStorage } from '@vueuse/core' 3 | import { DEFAULT_USER_CONFIG } from '@weibo-archiver/core' 4 | import { watch } from 'vue' 5 | 6 | const STORAGE_KEY = 'weibo-archiver' 7 | 8 | const createInitialConfig = (): UserConfig => (DEFAULT_USER_CONFIG) 9 | 10 | // 全局配置状态 11 | export const config = useStorage( 12 | STORAGE_KEY, 13 | createInitialConfig(), 14 | localStorage, 15 | { mergeDefaults: true }, 16 | ) 17 | 18 | export function useConfig() { 19 | function updateConfig(newConfig: Partial) { 20 | config.value = { 21 | ...config.value, 22 | ...newConfig, 23 | } 24 | } 25 | 26 | function toggleMinimize() { 27 | config.value.isMinimize = !config.value.isMinimize 28 | } 29 | 30 | function resetConfig() { 31 | const { user, isMinimize } = config.value 32 | config.value = { ...createInitialConfig(), user, isMinimize } 33 | } 34 | 35 | watch(config, (newConfig) => { 36 | const { startAt, endAt } = newConfig 37 | if (startAt && endAt) { 38 | newConfig.startAt = new Date(startAt).getTime() 39 | newConfig.endAt = new Date(endAt).getTime() 40 | } 41 | }, { immediate: true }) 42 | 43 | return { 44 | config, 45 | updateConfig, 46 | toggleMinimize, 47 | resetConfig, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/monkey/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from './App.vue' 4 | 5 | import './style.css' 6 | 7 | const app = createApp(App) 8 | 9 | const div = document.createElement('div') 10 | div.id = 'plugin-app' 11 | document.body.append(div) 12 | 13 | app.mount(div) 14 | 15 | console.log('weibo-archiver 加载成功') 16 | -------------------------------------------------------------------------------- /apps/monkey/src/style.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "daisyui" { 4 | themes: all; 5 | root: "#plugin-app"; 6 | log: true; 7 | } 8 | 9 | img { 10 | display: initial; 11 | } 12 | -------------------------------------------------------------------------------- /apps/monkey/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { UserInfo } from '@weibo-archiver/core' 2 | 3 | export interface FetchState { 4 | status: 'idle' | 'running' | 'finish' 5 | fetchType: 'weibo' | 'followings' | 'favorites' 6 | } 7 | 8 | export interface UserConfig { 9 | user?: UserInfo 10 | isMinimize: boolean 11 | restore: boolean 12 | isFetchAll: boolean 13 | largePic: boolean 14 | repostPic: boolean 15 | hasRepost: boolean 16 | hasComment: boolean 17 | commentCount: number 18 | 19 | hasFollowings: boolean 20 | hasFavorites: boolean 21 | hasWeibo: boolean 22 | 23 | startAt: number 24 | endAt: number 25 | curPage: number 26 | fetchedCount: number 27 | total: number 28 | theme: string 29 | } 30 | 31 | export type FetchOptions = Omit 32 | -------------------------------------------------------------------------------- /apps/monkey/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "jsxImportSource": "vue", 7 | "lib": ["DOM", "ESNext"], 8 | "useDefineForClassFields": true, 9 | "baseUrl": ".", 10 | "module": "ESNext", 11 | "moduleResolution": "Bundler", 12 | "paths": { 13 | "@weibo-archiver/core": ["../../packages/core"], 14 | "@/*": ["./src/*"] 15 | }, 16 | "types": [ 17 | "vite/client" 18 | ], 19 | "allowJs": true, 20 | "strict": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "esModuleInterop": true, 24 | "skipLibCheck": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web-v2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | !.env 7 | .idea/ 8 | 9 | 10 | *storybook.log 11 | -------------------------------------------------------------------------------- /apps/web-v2/.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/vue3-vite').StorybookConfig } */ 2 | const config = { 3 | stories: [ 4 | '../src/**/*.mdx', 5 | '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', 6 | ], 7 | addons: [ 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-onboarding', 10 | '@chromatic-com/storybook', 11 | '@storybook/experimental-addon-test', 12 | ], 13 | framework: { 14 | name: '@storybook/vue3-vite', 15 | options: {}, 16 | }, 17 | } 18 | export default config 19 | -------------------------------------------------------------------------------- /apps/web-v2/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { setup } from '@storybook/vue3' 2 | import { createPinia } from 'pinia' 3 | import components from '../src/stories/components' 4 | import '../src/style.css' 5 | 6 | setup((app) => { 7 | app.use(createPinia()) 8 | components.forEach((component) => { 9 | app.component(component.name, component) 10 | }) 11 | }) 12 | 13 | /** @type { import('@storybook/vue3').Preview } */ 14 | const preview = { 15 | parameters: { 16 | controls: { 17 | matchers: { 18 | color: /(background|color)$/i, 19 | date: /Date$/i, 20 | }, 21 | }, 22 | }, 23 | decorators: [ 24 | story => ({ 25 | components: { story }, 26 | template: ` 29 | 30 | `, 31 | }), 32 | ], 33 | } 34 | 35 | export default preview 36 | -------------------------------------------------------------------------------- /apps/web-v2/.storybook/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { setProjectAnnotations } from '@storybook/vue3' 2 | import { beforeAll } from 'vitest' 3 | import * as projectAnnotations from './preview' 4 | 5 | // This is an important step to apply the right configuration when testing your stories. 6 | // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations 7 | const project = setProjectAnnotations([projectAnnotations]) 8 | 9 | beforeAll(project.beforeAll) 10 | -------------------------------------------------------------------------------- /apps/web-v2/README.md: -------------------------------------------------------------------------------- 1 | ## Weibo-archiver 网页版 2 | 3 | 使用 Vite + Vue3 + naive-ui 构建,之后计划迁移到 Nuxt3 以使用 Sqlite3 做数据库层 4 | -------------------------------------------------------------------------------- /apps/web-v2/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "new-york", 4 | "typescript": true, 5 | "tailwind": { 6 | "config": "", 7 | "css": "src/style.css", 8 | "baseColor": "neutral", 9 | "cssVariables": true, 10 | "prefix": "" 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "composables": "@/composables", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib" 18 | }, 19 | "iconLibrary": "lucide" 20 | } 21 | -------------------------------------------------------------------------------- /apps/web-v2/docs/.vitepress/components/Giscus.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /apps/web-v2/docs/.vitepress/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web-v2/docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from 'vitepress' 2 | import DefaultTheme from 'vitepress/theme' 3 | import Giscus from '../components/Giscus.vue' 4 | import Layout from '../components/Layout.vue' 5 | 6 | export default { 7 | extends: DefaultTheme, 8 | enhanceApp({ app }) { 9 | app.component('Giscus', Giscus) 10 | }, 11 | Layout, 12 | } satisfies Theme 13 | -------------------------------------------------------------------------------- /apps/web-v2/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: "Weibo archiver" 6 | text: "在线备份你的微博" 7 | tagline: 将你的新浪微博回忆归档,为号被完全夹没前未雨绸缪 8 | image: https://weibo-archiver.chilfish.top/icon.webp 9 | actions: 10 | - theme: brand 11 | text: 介绍与上手 12 | link: /intro 13 | - theme: alt 14 | text: FAQ 15 | link: /faq 16 | 17 | features: 18 | - title: 快捷导出你的微博 19 | details: 不想开会员用官方那按麻烦的备份功能,且不够美观?那就对了,我也这么想的 20 | - title: 本地离线查看 21 | details: 微博数据只保存在浏览器本地里,图片也只留在本地硬盘里,别想再删我的微博了 22 | - title: 开源免费 23 | details: Apache-2.0 license 开源,不过也欢迎赞助V我一顿 24 | --- 25 | 26 | 31 | -------------------------------------------------------------------------------- /apps/web-v2/docs/intro.md: -------------------------------------------------------------------------------- 1 | # Weibo Archiver —— 备份你的微博 2 | 3 | ## 介绍 4 | 5 | 欢迎使用 Weibo Archiver!这是一款强大的微博备份工具,旨在帮助你永久珍藏那些在微博上发布的珍贵瞬间和重要信息。 6 | 7 | **主要功能:** 8 | 9 | - **免费导出:** 你可以免费导出所有在微博网页版上**可查看**的微博内容。只要你在浏览器登录微博后能看到的,它就能帮你备份下来。 10 | - **高清图片:** 支持备份微博中的高清图片数据。 11 | - **可视化查看:** 提供了一个清爽的网页界面,让你方便地导入和浏览已备份的微博数据。 12 | - **多种工具:** 项目包含油猴脚本(适用于浏览器快速抓取)、本地服务器(用于下载图片和离线查看)以及命令行工具(CLI,适用于更高级的定制化备份)。 13 | - **开源免费:** 本项目采用 Apache 2.0 开源协议,完全免费,没有任何隐藏收费。如果你希望二次创作或引用,请记得注明出处和署名,并遵守开源协议。 14 | 15 | ## 安装与上手指南 16 | 17 | 要开始使用 Weibo Archiver,最简单的方式是从“油猴脚本”入手。下面我们一步步教你如何操作。 18 | 19 | **本工具是如何工作的?** 20 | 21 | 简单来说,Weibo Archiver 提供了几种方式来获取和查看你的微博数据: 22 | 23 | 1. **油猴脚本 (Tampermonkey Script):** 这是最推荐给普通用户的方式。它是一个浏览器插件的小脚本,可以直接在你的微博页面上运行,帮你把看到的微博内容抓取下来。 24 | 2. **网页端导入查看 (Web Viewer):** 这是一个专门的网页,你可以把油猴脚本导出的数据文件上传到这里,就能像浏览真实微博一样查看你备份的内容。 25 | 3. **本地服务器 (Local Server):** 这是一个小程序,它能帮你下载微博中的图片到你的电脑上,并且在你的电脑上搭建一个“迷你网站”,让你可以在没有网络的情况下查看备份的微博和图片。 26 | 4. **CLI 爬取工具 (Command Line Interface):** 这是为有一定技术基础的用户准备的。通过在电脑的“命令行终端”输入指令,可以更灵活地备份微博,例如指定备份某个用户、某个时间段的微博等。 27 | 28 | ## 从旧版本迁移 29 | 30 | 网站工具做了很大改变的升级迁移,如果你在 [旧版本](https://weibo.chilfish.top) 中导出过微博数据,只要在新的 [网页版](https://weibo-archiver.chilfish.top/post) 再次导入 weibo json 文件即可,它会自动完成数据迁移 31 | 32 | 目前新版油猴脚本导出的微博数据相比旧版没新增其他额外的数据,不过像是关注列表、收藏夹这些就要重新导出了 33 | 34 | ## 赞助V我 35 | 36 | 如果你觉得这个项目对你有帮助,可以考虑赞助v我😇这将给我更多的动力来维护这个项目:[赞助地址](https://chilfish.top/sponsors) 37 | -------------------------------------------------------------------------------- /apps/web-v2/docs/monkey.md: -------------------------------------------------------------------------------- 1 | # 油猴脚本 2 | 3 | 油猴脚本是 Weibo Archiver 项目中最易于上手的工具,它直接在你的浏览器中运行,让你在浏览微博的同时就能方便地进行备份。 4 | 5 | ## 如何使用油猴脚本备份微博? 6 | 7 | 首先需要在浏览器中安装 [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) 插件,建议使用较新版本的浏览器,以免出现页面混乱等问题 8 | 9 | 然后安装 [weibo-archiver.user.js](https://p.chilfish.top/weibo/weibo-archiver.user.js) 脚本文件,在微博网页版按以下步骤运行即可。 10 | 11 | 1. **确保已登录微博:** 在使用脚本前,请务必先在浏览器中登录你的微博网页版 (https://weibo.com)。在页面的右上角会有脚本的logo。 12 | 2. **启动备份:** 点击脚本提供的备份按钮。你可能看到一些选项,例如: 13 | - 是否包含评论 14 | - 是否按时间范围筛选 15 | - 是否继续上次的记录(如果之前备份中断过) 16 | 3. **耐心等待:** 脚本会自动翻页并抓取微博内容。 17 | 4. **保存结果:** 备份完成后,脚本会提示你下载数据文件。通常包含: 18 | - `weibo-data.json`:这是主要的微博数据文件,包含了微博文字、时间、转发、评论(部分)等信息。 19 | - `imgs.csv`:这是一个记录了所有微博中图片原始链接的文件。你需要配合后面的“本地服务器”工具来下载这些图片。 20 | 21 | 之后就能将这份 json 文件导入到 [在线网页版](https://weibo-archiver.chilfish.top/post) 中查看了🥳 22 | -------------------------------------------------------------------------------- /apps/web-v2/docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # 路线图(画饼) 2 | 3 | 一些可能会做的功能,感觉可能会发展成又一个第三方微博的客户端,不过还是以保存数据为主要 4 | 5 | - [ ] 使用 Chrome 浏览器插件来实现静默自动备份以及其他更方便的操作 6 | - [ ] 我发出评论:https://weibo.com/comment/outbox 7 | - [ ] 楼中楼实现 8 | - [ ] WebDav 等自定义数据存储服务 9 | -------------------------------------------------------------------------------- /apps/web-v2/docs/web.md: -------------------------------------------------------------------------------- 1 | # 网页端导入查看 2 | 3 | 当你使用油猴脚本成功导出 `weibo-data.json` 文件后,就可以通过 Weibo Archiver 提供的网页端查看器来浏览你的备份了。 4 | 5 | ## 如何使用? 6 | 7 | 1. **打开预览页面:** 在浏览器中打开 [你的在线预览页面链接](请替换为实际的预览页面网址)。 8 | 2. **导入数据:** 页面上通常会有一个“导入”或“上传”按钮。点击它,然后选择你之前保存的 `weibo-data.json` 文件。 9 | 3. **浏览微博:** 数据导入后,页面就会以类似微博时间线的形式展示你备份的内容。你可以滚动查看、搜索(如果支持)等。 10 | 11 | ## 核心特性 12 | 13 | - **数据本地化,隐私安全:** 你上传的 `weibo-data.json` 文件是**完全保存在你的浏览器本地内存中的**。这意味着数据不会上传到任何服务器,只有你自己能看到。关闭浏览器或刷新页面后,需要重新导入。别人无法通过链接直接查看你的备份数据。 14 | - **数据合并:** 如果你因为微博数量太多而分多次导出了多个 `weibo-data.json` 文件(例如,按不同年份导出),你可以将这些文件**依次导入**到预览页面。工具会自动帮你合并这些数据。合并完成后,预览页面通常会提供一个“导出数据”的按钮,点击后可以得到一个包含所有微博的、完整的 `weibo-data.json` 文件。 15 | - **图片显示:** 16 | - **在线链接:** 默认情况下,预览页面会尝试加载 `imgs.csv` 文件中记录的原始图片链接。如果这些链接仍然有效,你就能看到图片。 17 | - **本地图片(推荐):** 为了长期可靠地查看图片(防止原链接失效),强烈建议你使用下一章节介绍的“本地服务器”工具下载所有图片到电脑,并通过本地服务器来查看。 18 | - **自定义图床:** 如果你将下载的图片上传到了自己的图床(一种在线存储图片的服务),可以在预览页面的设置中填写你的图床基础链接。注意,上传到图床时请保持图片的文件名和相对路径不变,以便预览器能正确找到它们。 19 | -------------------------------------------------------------------------------- /apps/web-v2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Weibo Archvier 8 | 9 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web-v2/public/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chilfish/Weibo-archiver/1d4c7501b387c3375eca5bacd76cd94736700469/apps/web-v2/public/icon.webp -------------------------------------------------------------------------------- /apps/web-v2/public/placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chilfish/Weibo-archiver/1d4c7501b387c3375eca5bacd76cd94736700469/apps/web-v2/public/placeholder.webp -------------------------------------------------------------------------------- /apps/web-v2/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/DatePicker.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | {{ value ? df.format(value.toDate(getLocalTimeZone())) : "请选一个日期" }} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/EmptyWeibo.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 10 | 11 | 欢迎使用 Weibo Archiver 12 | 13 | 14 | 开始备份您的微博记忆,防止珍贵内容丢失 15 | 16 | 17 | 20 | 23 | 24 | 25 | 导入数据 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/album/AlbumPhotos.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 30 | 35 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/album/AlbumPreviewWeibo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 暂无评论 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/common/Avatar.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 17 | 22 | {{ props.alt }} 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/common/BackToTop.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 25 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/common/OpenSetting.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 设置 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/common/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 15 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/followings/DataTableDropdown.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | Open menu 19 | 20 | 21 | 22 | 23 | Actions 24 | 25 | Copy payment ID 26 | 27 | 28 | View customer 29 | View payment details 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/followings/FollowingsTable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/settings/ImageSourceOption.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | imgHost = value" 25 | > 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | {{ label }} 43 | 44 | 45 | {{ description }} 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/settings/Settings.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/settings/SettingsHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 设置 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/settings/SettingsSidebar.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 35 | 42 | 43 | {{ item.label }} 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/accordion/Accordion.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/accordion/AccordionContent.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/accordion/AccordionItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/accordion/AccordionTrigger.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Accordion } from './Accordion.vue' 2 | export { default as AccordionContent } from './AccordionContent.vue' 3 | export { default as AccordionItem } from './AccordionItem.vue' 4 | export { default as AccordionTrigger } from './AccordionTrigger.vue' 5 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/avatar/AvatarFallback.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/avatar/AvatarImage.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar.vue' 2 | export { default as AvatarFallback } from './AvatarFallback.vue' 3 | export { default as AvatarImage } from './AvatarImage.vue' 4 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from 'class-variance-authority' 2 | import { cva } from 'class-variance-authority' 3 | 4 | export { default as Badge } from './Badge.vue' 5 | 6 | export const badgeVariants = cva( 7 | 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', 13 | secondary: 14 | 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', 15 | destructive: 16 | 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 17 | outline: 18 | 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: 'default', 23 | }, 24 | }, 25 | ) 26 | export type BadgeVariants = VariantProps 27 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarCell.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarGrid.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarGridBody.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarGridHead.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarGridRow.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarHeadCell.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarHeader.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarHeading.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 30 | 31 | {{ headingValue }} 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarNextButton.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/CalendarPrevButton.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/calendar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Calendar } from './Calendar.vue' 2 | export { default as CalendarCell } from './CalendarCell.vue' 3 | export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue' 4 | export { default as CalendarGrid } from './CalendarGrid.vue' 5 | export { default as CalendarGridBody } from './CalendarGridBody.vue' 6 | export { default as CalendarGridHead } from './CalendarGridHead.vue' 7 | export { default as CalendarGridRow } from './CalendarGridRow.vue' 8 | export { default as CalendarHeadCell } from './CalendarHeadCell.vue' 9 | export { default as CalendarHeader } from './CalendarHeader.vue' 10 | export { default as CalendarHeading } from './CalendarHeading.vue' 11 | export { default as CalendarNextButton } from './CalendarNextButton.vue' 12 | export { default as CalendarPrevButton } from './CalendarPrevButton.vue' 13 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardAction.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardAction } from './CardAction.vue' 3 | export { default as CardContent } from './CardContent.vue' 4 | export { default as CardDescription } from './CardDescription.vue' 5 | export { default as CardFooter } from './CardFooter.vue' 6 | export { default as CardHeader } from './CardHeader.vue' 7 | export { default as CardTitle } from './CardTitle.vue' 8 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/collapsible/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/collapsible/CollapsibleContent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/collapsible/CollapsibleTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapsible } from './Collapsible.vue' 2 | export { default as CollapsibleContent } from './CollapsibleContent.vue' 3 | export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue' 4 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogOverlay.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogOverlay } from './DialogOverlay.vue' 8 | export { default as DialogProvider } from './DialogProvider.vue' 9 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 10 | export { default as DialogTitle } from './DialogTitle.vue' 11 | export { default as DialogTrigger } from './DialogTrigger.vue' 12 | 13 | export { useDialog } from './useDialog' 14 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dialog/useDialog.ts: -------------------------------------------------------------------------------- 1 | import type { DialogOptions } from './DialogProvider.vue' 2 | import { inject } from 'vue' 3 | 4 | interface DialogControl { 5 | openDialog: (id: string, options: DialogOptions) => void 6 | closeDialog: (id: string) => void 7 | registerDialog: (id: string) => void 8 | dialogs: Record 9 | } 10 | 11 | export function useDialog() { 12 | const dialogControl = inject('dialogs') 13 | 14 | if (!dialogControl) { 15 | throw new Error('useDialog must be used within a DialogProvider') 16 | } 17 | 18 | return [ 19 | dialogControl.openDialog, 20 | dialogControl.closeDialog, 21 | dialogControl.registerDialog, 22 | dialogControl.dialogs, 23 | ] as const 24 | } 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuContent.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DropdownMenu } from './DropdownMenu.vue' 2 | 3 | export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' 4 | export { default as DropdownMenuContent } from './DropdownMenuContent.vue' 5 | export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' 6 | export { default as DropdownMenuItem } from './DropdownMenuItem.vue' 7 | export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' 8 | export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' 9 | export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' 10 | export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' 11 | export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' 12 | export { default as DropdownMenuSub } from './DropdownMenuSub.vue' 13 | export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' 14 | export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' 15 | export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' 16 | export { DropdownMenuPortal } from 'reka-ui' 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 33 | 34 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/popover/Popover.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/popover/PopoverAnchor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/popover/PopoverContent.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 37 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/popover/PopoverTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Popover } from './Popover.vue' 2 | export { default as PopoverAnchor } from './PopoverAnchor.vue' 3 | export { default as PopoverContent } from './PopoverContent.vue' 4 | export { default as PopoverTrigger } from './PopoverTrigger.vue' 5 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/radio-group/RadioGroup.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/radio-group/RadioGroupItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 32 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RadioGroup } from './RadioGroup.vue' 2 | export { default as RadioGroupItem } from './RadioGroupItem.vue' 3 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/scroll-area/ScrollArea.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/scroll-area/ScrollBar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/scroll-area/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ScrollArea } from './ScrollArea.vue' 2 | export { default as ScrollBar } from './ScrollBar.vue' 3 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue' 2 | export { default as SelectContent } from './SelectContent.vue' 3 | export { default as SelectGroup } from './SelectGroup.vue' 4 | export { default as SelectItem } from './SelectItem.vue' 5 | export { default as SelectItemText } from './SelectItemText.vue' 6 | export { default as SelectLabel } from './SelectLabel.vue' 7 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' 8 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' 9 | export { default as SelectSeparator } from './SelectSeparator.vue' 10 | export { default as SelectTrigger } from './SelectTrigger.vue' 11 | export { default as SelectValue } from './SelectValue.vue' 12 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/Sheet.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetClose.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetOverlay.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/SheetTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sheet } from './Sheet.vue' 2 | export { default as SheetClose } from './SheetClose.vue' 3 | export { default as SheetContent } from './SheetContent.vue' 4 | export { default as SheetDescription } from './SheetDescription.vue' 5 | export { default as SheetFooter } from './SheetFooter.vue' 6 | export { default as SheetHeader } from './SheetHeader.vue' 7 | export { default as SheetTitle } from './SheetTitle.vue' 8 | export { default as SheetTrigger } from './SheetTrigger.vue' 9 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarGroupAction.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarGroupContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarGroupLabel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarInset.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuAction.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuBadge.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuButton.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | {{ tooltip }} 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuButtonChild.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuSkeleton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 23 | 28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuSub.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuSubButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarMenuSubItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarRail.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/SidebarTrigger.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 24 | 25 | Toggle Sidebar 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/sidebar/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref } from 'vue' 2 | import { createContext } from 'reka-ui' 3 | 4 | export const SIDEBAR_COOKIE_NAME = 'sidebar:state' 5 | export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 6 | export const SIDEBAR_WIDTH = '12rem' 7 | export const SIDEBAR_WIDTH_MOBILE = '18rem' 8 | export const SIDEBAR_WIDTH_ICON = '3rem' 9 | export const SIDEBAR_KEYBOARD_SHORTCUT = 'b' 10 | 11 | export const [useSidebar, provideSidebarContext] = createContext<{ 12 | state: ComputedRef<'expanded' | 'collapsed'> 13 | open: Ref 14 | setOpen: (value: boolean) => void 15 | isMobile: Ref 16 | openMobile: Ref 17 | setOpenMobile: (value: boolean) => void 18 | toggleSidebar: () => void 19 | }>('Sidebar') 20 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/skeleton/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeleton } from './Skeleton.vue' 2 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/switch/Switch.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 35 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Switch } from './Switch.vue' 2 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/Table.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableBody.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableCaption.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableCell.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableEmpty.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableHead.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/TableRow.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Table } from './Table.vue' 2 | export { default as TableBody } from './TableBody.vue' 3 | export { default as TableCaption } from './TableCaption.vue' 4 | export { default as TableCell } from './TableCell.vue' 5 | export { default as TableEmpty } from './TableEmpty.vue' 6 | export { default as TableFooter } from './TableFooter.vue' 7 | export { default as TableHead } from './TableHead.vue' 8 | export { default as TableHeader } from './TableHeader.vue' 9 | export { default as TableRow } from './TableRow.vue' 10 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/table/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Updater } from '@tanstack/vue-table' 2 | import type { Ref } from 'vue' 3 | 4 | export function valueUpdater>(updaterOrValue: T, ref: Ref) { 5 | ref.value 6 | = typeof updaterOrValue === 'function' 7 | ? updaterOrValue(ref.value) 8 | : updaterOrValue 9 | } 10 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tabs/TabsTrigger.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs.vue' 2 | export { default as TabsContent } from './TabsContent.vue' 3 | export { default as TabsList } from './TabsList.vue' 4 | export { default as TabsTrigger } from './TabsTrigger.vue' 5 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tooltip/TooltipContent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue' 2 | export { default as TooltipContent } from './TooltipContent.vue' 3 | export { default as TooltipProvider } from './TooltipProvider.vue' 4 | export { default as TooltipTrigger } from './TooltipTrigger.vue' 5 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/weibo/Weibo.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 16 | 20 | 21 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/weibo/WeiboActions.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 29 | 30 | {{ formatNumber(actions.likes) }} 31 | 32 | 37 | 38 | {{ formatNumber(actions.comments) }} 39 | 40 | 45 | 46 | {{ formatNumber(actions.reposts) }} 47 | 48 | 49 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/weibo/WeiboEmoji.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 21 | 25 | {{ emojiPhrase }} 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/weibo/WeiboLinkCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 23 | 29 | 30 | 31 | {{ card.title }} 32 | 33 | 34 | {{ desc }} 35 | 36 | 37 | {{ props.card.link }} 38 | 39 | 40 | 41 | 42 | 43 | 62 | -------------------------------------------------------------------------------- /apps/web-v2/src/components/weibo/WeiboRetweet.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 26 | 27 | 30 | 31 | 37 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/web-v2/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Updater } from '@tanstack/vue-table' 2 | import type { ClassValue } from 'clsx' 3 | 4 | import type { Ref } from 'vue' 5 | import { clsx } from 'clsx' 6 | import { twMerge } from 'tailwind-merge' 7 | 8 | export function cn(...inputs: ClassValue[]) { 9 | return twMerge(clsx(inputs)) 10 | } 11 | 12 | export function valueUpdater>(updaterOrValue: T, ref: Ref) { 13 | ref.value = typeof updaterOrValue === 'function' 14 | ? updaterOrValue(ref.value) 15 | : updaterOrValue 16 | } 17 | -------------------------------------------------------------------------------- /apps/web-v2/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createHead } from '@unhead/vue/client' 2 | import { createPinia } from 'pinia' 3 | import { createApp } from 'vue' 4 | import App from './App.vue' 5 | import routes from './routes' 6 | 7 | import './style.css' 8 | 9 | createApp(App) 10 | .use(routes) 11 | .use(createHead()) 12 | .use(createPinia()) 13 | .mount('#app') 14 | -------------------------------------------------------------------------------- /apps/web-v2/src/pages/followings.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 25 | 关注列表管理 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /apps/web-v2/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/web-v2/src/pages/memos.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 26 | 29 | 微博回忆——那年今日 ({{ weiboArr.length }}件) 30 | 31 | 32 | 36 | 41 | 42 | 43 | 47 | 暂时未发现过往在这一天发过的微博哦 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /apps/web-v2/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import { defineAsyncComponent, h } from 'vue' 3 | import { 4 | createRouter, 5 | createWebHistory, 6 | 7 | } from 'vue-router' 8 | 9 | function useDefaultRoute(name = '') { 10 | const page = name || 'index' 11 | return { 12 | path: `/${name}`, 13 | name: page, 14 | component: h(defineAsyncComponent(() => import(`../pages/${page}.vue`))), 15 | } as RouteRecordRaw 16 | } 17 | 18 | const routes: RouteRecordRaw[] = [ 19 | '', 20 | 'post', 21 | 'album', 22 | 'bookmarks', 23 | 'memos', 24 | 'followings', 25 | 'search', 26 | ].map(useDefaultRoute) 27 | 28 | routes.push({ 29 | name: '404', 30 | path: '/:pathMatch(.*)*', 31 | redirect: '/', 32 | }) 33 | 34 | const router = createRouter({ 35 | history: createWebHistory(import.meta.env.BASE_URL), 36 | routes, 37 | }) 38 | 39 | export default router 40 | -------------------------------------------------------------------------------- /apps/web-v2/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './postStore' 2 | export * from './userStore' 3 | -------------------------------------------------------------------------------- /apps/web-v2/src/stores/userStore.ts: -------------------------------------------------------------------------------- 1 | import type { Following, UserInfo } from '@weibo-archiver/core' 2 | import { useStorage } from '@vueuse/core' 3 | import { idb } from '@weibo-archiver/core' 4 | import { defineStore } from 'pinia' 5 | import { ref } from 'vue' 6 | 7 | export const useUserStore = defineStore('user', () => { 8 | const curUid = useStorage('curUid', '') 9 | const users = ref([]) 10 | const curUser = ref({} as unknown as UserInfo) 11 | const isLoadingUser = ref(false) 12 | 13 | async function load() { 14 | if (curUid.value) { 15 | await idb.setCurUser(curUid.value) 16 | } 17 | 18 | users.value = await idb.getUsers() 19 | curUser.value = idb.curUser 20 | } 21 | 22 | async function setCurUid(uid: string) { 23 | isLoadingUser.value = true 24 | curUid.value = uid 25 | await idb.setCurUser(uid) 26 | curUser.value = idb.curUser 27 | isLoadingUser.value = false 28 | } 29 | 30 | async function addUser(user: UserInfo | null | undefined) { 31 | if (!user) { 32 | return 33 | } 34 | 35 | await idb.addUser(user) 36 | } 37 | 38 | async function importUser(user: UserInfo) { 39 | await addUser(user) 40 | await setCurUid(user.uid) 41 | } 42 | 43 | async function getFollowings(): Promise { 44 | return idb.getFollowings() 45 | } 46 | 47 | async function getAllUsers(): Promise { 48 | return idb.getUsers() 49 | } 50 | 51 | return { 52 | users, 53 | curUser, 54 | curUid, 55 | isLoadingUser, 56 | addUser, 57 | importUser, 58 | load, 59 | setCurUid, 60 | getFollowings, 61 | getAllUsers, 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/Album.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import AlbumPhotos from '@/components/album/AlbumPhotos.vue' 3 | 4 | const meta: Meta = { 5 | title: 'Components/Album', 6 | component: AlbumPhotos, 7 | } 8 | 9 | export default meta 10 | 11 | type Story = StoryObj 12 | 13 | export const Default: Story = { 14 | render: () => ({ 15 | components: { AlbumPhotos }, 16 | template: ` 17 | 18 | `, 19 | }), 20 | } 21 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/ImageGallery.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import ImageGallery from '../components/common/ImageGallery.vue' 3 | import { images } from './test.data' 4 | 5 | const meta: Meta = { 6 | title: 'Components/ImageGallery', 7 | component: ImageGallery, 8 | } 9 | 10 | export default meta 11 | 12 | type Story = StoryObj 13 | 14 | export const Default: Story = { 15 | render: () => ({ 16 | components: { ImageGallery }, 17 | template: /* html */` 18 | 19 | 20 | 21 | 22 | `, 23 | setup() { 24 | return { 25 | images, 26 | } 27 | }, 28 | }), 29 | } 30 | 31 | export const SingleImage: Story = { 32 | render: () => ({ 33 | components: { ImageGallery }, 34 | template: '', 35 | setup() { 36 | return { 37 | images: images.slice(0, 1), 38 | } 39 | }, 40 | }), 41 | } 42 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/LazyImage.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import LazyImage from '../components/common/LazyImage.vue' 3 | 4 | const meta: Meta = { 5 | title: 'Components/LazyImage', 6 | component: LazyImage, 7 | } 8 | 9 | export default meta 10 | 11 | type Story = StoryObj 12 | 13 | export const Default: Story = { 14 | render: () => ({ 15 | components: { LazyImage }, 16 | template: /* html */ ` 17 | 18 | 21 | 22 | `, 23 | }), 24 | } 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/Settings.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import Settings from '../components/settings/Settings.vue' 3 | 4 | const meta: Meta = { 5 | title: 'Components/Settings', 6 | component: Settings, 7 | } 8 | 9 | export default meta 10 | 11 | type Story = StoryObj 12 | 13 | export const Default: Story = { 14 | render: () => ({ 15 | components: { Settings }, 16 | template: /* html */ ` 17 | 20 | 21 | 22 | `, 23 | }), 24 | } 25 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/SwitchUser.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import SwitchUser from '../components/SwitchUser.vue' 3 | import { users } from './test.data' 4 | 5 | const meta: Meta = { 6 | title: 'Components/SwitchUser', 7 | component: SwitchUser, 8 | } 9 | 10 | type Story = StoryObj 11 | 12 | export default meta 13 | 14 | export const Default: Story = { 15 | render: () => ({ 16 | components: { SwitchUser }, 17 | template: /* html */` 18 | 19 | 用户切换 20 | 21 | 22 | `, 23 | setup() { 24 | return { 25 | users, 26 | } 27 | }, 28 | }), 29 | } 30 | 31 | export const NoUser: Story = { 32 | render: () => ({ 33 | components: { SwitchUser }, 34 | template: /* html */` 35 | 36 | 没有用户数据 37 | 38 | 39 | `, 40 | }), 41 | } 42 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/components/RouterLink.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/components/index.ts: -------------------------------------------------------------------------------- 1 | import RouterLink from './RouterLink.vue' 2 | 3 | const components = [ 4 | RouterLink, 5 | ] 6 | 7 | export default components 8 | -------------------------------------------------------------------------------- /apps/web-v2/src/stories/shadcn/button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/vue3' 2 | import { Button } from '@/components/ui/button' 3 | 4 | const meta: Meta = { 5 | title: 'Shadcn/Button', 6 | component: Button, 7 | } 8 | 9 | export default meta 10 | 11 | type Story = StoryObj 12 | 13 | export const Default: Story = { 14 | render: () => ({ 15 | components: { Button }, 16 | template: ` 20 | Hello 21 | `, 22 | }), 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-v2/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Post } from '@weibo-archiver/core' 2 | 3 | export interface ImagePreviewEvent { 4 | index: number 5 | imgs: string[] 6 | } 7 | 8 | export interface AlbumPreviewEvent { 9 | idxOfPost: number 10 | idxOfImg: number 11 | posts: Post[] 12 | } 13 | 14 | export interface AppConfig { 15 | theme: string 16 | imgHost: 'cdn' | 'original' | 'local' | 'custom' 17 | customImageUrl: string 18 | } 19 | -------------------------------------------------------------------------------- /apps/web-v2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "jsxImportSource": "vue", 7 | "lib": ["DOM", "ESNext"], 8 | "useDefineForClassFields": true, 9 | "baseUrl": ".", 10 | "module": "ESNext", 11 | "moduleResolution": "Bundler", 12 | "paths": { 13 | "@weibo-archiver/core": ["../../packages/core"], 14 | "@/*": ["./src/*"] 15 | }, 16 | "types": [ 17 | "vite/client" 18 | ], 19 | "allowJs": true, 20 | "strict": true, 21 | "strictNullChecks": true, 22 | "noUnusedLocals": true, 23 | "esModuleInterop": true, 24 | "skipLibCheck": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web-v2/vercel-build.js: -------------------------------------------------------------------------------- 1 | const { 2 | VERCEL_GIT_COMMIT_REF, 3 | VERCEL_GIT_COMMIT_MESSAGE, 4 | } = process.env 5 | 6 | // 在 main 分支,commit message 包含 web,或是 release、update deps 7 | // 在包含 web 的任何分支 8 | // 若不在 main 或 web 分支,则只要 commit message 包含 web 9 | 10 | const messages = [ 11 | 'release', 12 | 'update deps', 13 | 'web', 14 | ] 15 | 16 | const shouldProceed = messages.some((message) => { 17 | return VERCEL_GIT_COMMIT_MESSAGE.includes(message) 18 | }) 19 | 20 | const isMainBranch = VERCEL_GIT_COMMIT_REF === 'main' 21 | const isWebBranch = VERCEL_GIT_COMMIT_REF.includes('web') 22 | 23 | if ( 24 | (isMainBranch && shouldProceed) 25 | || isWebBranch 26 | || shouldProceed 27 | ) { 28 | console.log('✅ - Build can proceed') 29 | process.exit(1) 30 | } 31 | else { 32 | console.log('🛑 - Build cancelled') 33 | process.exit(0) 34 | } 35 | -------------------------------------------------------------------------------- /apps/web-v2/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ], 8 | "redirects": [ 9 | { 10 | "source": "/monkey", 11 | "destination": "https://p.chilfish.top/weibo/weibo-archiver.user.js", 12 | "statusCode": 301 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /apps/web-v2/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin' 5 | 6 | import { defineWorkspace } from 'vitest/config' 7 | 8 | const dirname 9 | = typeof __dirname !== 'undefined' 10 | ? __dirname 11 | : path.dirname(fileURLToPath(import.meta.url)) 12 | 13 | // More info at: https://storybook.js.org/docs/writing-tests/test-addon 14 | export default defineWorkspace([ 15 | 'vite.config.ts', 16 | { 17 | extends: 'vite.config.ts', 18 | plugins: [ 19 | // The plugin will run tests for the stories defined in your Storybook config 20 | // See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest 21 | storybookTest({ configDir: path.join(dirname, '.storybook') }), 22 | ], 23 | test: { 24 | name: 'storybook', 25 | browser: { 26 | enabled: true, 27 | headless: true, 28 | name: 'chromium', 29 | provider: 'playwright', 30 | }, 31 | setupFiles: ['.storybook/vitest.setup.ts'], 32 | }, 33 | }, 34 | ]) 35 | -------------------------------------------------------------------------------- /apps/web/vercel-build.js: -------------------------------------------------------------------------------- 1 | console.log('🛑 - Build cancelled') 2 | process.exit(0) 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | // unocss: true, 5 | formatters: true, 6 | vue: true, 7 | rules: { 8 | 'no-console': 'off', 9 | 'no-alert': 'off', 10 | 'no-confirm': 'off', 11 | 'vue/no-multiple-template-root': 'off', 12 | 'node/prefer-global/process': 'off', 13 | 'format/prettier': 'off', 14 | 'antfu/no-import-dist': 'off', 15 | 'antfu/top-level-function': 'off', 16 | 'antfu/no-top-level-await': 'off', 17 | 'unused-imports/no-unused-vars': 'warn', 18 | }, 19 | ignores: [ 20 | 'pnpm-lock.yaml', 21 | ], 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | ### @weibo-archiver/core 模块 2 | 3 | 项目的核心包,包含了 vue 与 dom 的部分 4 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@weibo-archiver/core", 3 | "type": "module", 4 | "private": true, 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "typecheck": "tsc --noEmit -p ./tsconfig.json" 8 | }, 9 | "dependencies": { 10 | "@internationalized/date": "^3.8.0", 11 | "axios": "^1.9.0", 12 | "dexie": "^4.0.11", 13 | "file-saver": "^2.0.5", 14 | "fuse.js": "^7.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './services' 3 | export type * from './types' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /packages/core/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetchService' 2 | export * from './parseService' 3 | export * from './postService' 4 | export * from './userService' 5 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Favorite, Post } from './post' 2 | import type { Following, UserInfo } from './user' 3 | 4 | export * from './fetchArgs' 5 | export * from './post' 6 | export * from './raw' 7 | export * from './user' 8 | 9 | export interface FetchConfig { 10 | isFetchAll: boolean 11 | restore: boolean 12 | hasRepost: boolean 13 | hasComment: boolean 14 | repostPic: boolean 15 | commentCount: number 16 | 17 | hasFollowings: boolean 18 | hasFavorites: boolean 19 | hasWeibo: boolean 20 | 21 | sinceId: string 22 | startAt: number 23 | endAt: number 24 | curPage: number 25 | } 26 | 27 | export interface UserConfig extends FetchConfig { 28 | user?: UserInfo 29 | isMinimize: boolean 30 | fetchedCount: number 31 | total: number 32 | theme: string 33 | } 34 | 35 | export interface Album { 36 | url: string 37 | id: string 38 | date: string 39 | } 40 | 41 | export interface ImportedData { 42 | weibo: Post[] 43 | user: UserInfo 44 | followings: Following[] 45 | favorites: Favorite[] 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/types/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod' 2 | import { userSchema } from './user' 3 | 4 | const linkCardSchema = z.object({ 5 | link: z.string(), 6 | title: z.string(), 7 | img: z.string(), 8 | desc: z.string().optional(), 9 | }) 10 | 11 | const postMetaSchema = z.object({ 12 | id: z.string(), 13 | regionName: z.string(), 14 | createdAt: z.string(), 15 | source: z.string().optional(), 16 | }) 17 | 18 | const commentSchema = z.object({ 19 | id: z.string(), 20 | text: z.string(), 21 | createdAt: z.string(), 22 | regionName: z.string(), 23 | likesCount: z.number(), 24 | commentsCount: z.number(), 25 | floor: z.number(), 26 | user: userSchema, 27 | img: z.string().optional(), 28 | }) 29 | 30 | const _postSchema = postMetaSchema.extend({ 31 | mblogid: z.string(), 32 | text: z.string(), 33 | imgs: z.array(z.string()), 34 | 35 | repostsCount: z.number(), 36 | commentsCount: z.number(), 37 | likesCount: z.number(), 38 | }) 39 | 40 | const retweetSchema = _postSchema.extend({ 41 | user: userSchema.optional(), 42 | }) 43 | 44 | const postSchema = _postSchema.extend({ 45 | userId: z.string(), 46 | card: linkCardSchema.optional(), 47 | comments: z.array(commentSchema), 48 | retweet: retweetSchema.optional(), 49 | isShowBulletIn: z.enum(['0', '2']), 50 | }) 51 | 52 | export type LinkCard = z.infer 53 | export type PostMeta = z.infer 54 | export type Post = z.infer 55 | export type Retweet = z.infer 56 | export type Comment = z.infer 57 | export type Favorite = Post 58 | -------------------------------------------------------------------------------- /packages/core/src/types/raw/index.ts: -------------------------------------------------------------------------------- 1 | export * from './favorites/all_fav' 2 | 3 | export * from './friendships/friends' 4 | export * from './profile/detail' 5 | export * from './profile/followContent' 6 | 7 | export * from './profile/info' 8 | 9 | export * from './side/search' 10 | export * from './statuses/buildComments' 11 | export * from './statuses/longtext' 12 | 13 | export * from './statuses/mymblog' 14 | -------------------------------------------------------------------------------- /packages/core/src/types/raw/profile/detail.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const RawUserDetailSchema = z.object({ 4 | sunshine_credit: z.object({ level: z.string() }), 5 | birthday: z.string(), 6 | created_at: z.string(), 7 | description: z.string(), 8 | gender: z.string(), 9 | location: z.string(), 10 | real_name: z.object({ name: z.string(), career: z.string() }), 11 | career: z.object({ company: z.string() }), 12 | company: z.string(), 13 | followers: z.object({ 14 | total_number: z.number(), 15 | users: z.array( 16 | z.object({ 17 | screen_name: z.string(), 18 | avatar_large: z.string(), 19 | id: z.number(), 20 | }), 21 | ), 22 | }), 23 | label_desc: z.array( 24 | z.object({ 25 | name: z.string(), 26 | normal_mode: z.object({ 27 | word_color: z.string(), 28 | background_color: z.string(), 29 | }), 30 | dark_mode: z.object({ 31 | word_color: z.string(), 32 | background_color: z.string(), 33 | }), 34 | scheme_url: z.string(), 35 | }), 36 | ), 37 | desc_text: z.string(), 38 | verified_url: z.string(), 39 | friend_info: z.string(), 40 | companyVerified: z.object({ 41 | licenseCode: z.string(), 42 | serviceType: z.string(), 43 | realName: z.string(), 44 | }), 45 | links: z.array(z.object({ url: z.string(), title: z.string() })), 46 | }) 47 | 48 | export type RawUserDetail = z.infer 49 | -------------------------------------------------------------------------------- /packages/core/src/types/raw/statuses/longtext.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod' 2 | 3 | const ActionlogSchema = z.object({ 4 | act_type: z.number(), 5 | act_code: z.number(), 6 | oid: z.string(), 7 | uuid: z.number(), 8 | cardid: z.string(), 9 | lcardid: z.string(), 10 | uicode: z.string(), 11 | luicode: z.string(), 12 | fid: z.string(), 13 | lfid: z.string(), 14 | ext: z.string(), 15 | }) 16 | type Actionlog = z.infer 17 | 18 | const TopicStructSchema = z.object({ 19 | title: z.string(), 20 | topic_url: z.string(), 21 | topic_title: z.string(), 22 | actionlog: ActionlogSchema, 23 | }) 24 | type TopicStruct = z.infer 25 | 26 | const RawLongTextSchema = z.object({ 27 | longTextContent: z.string(), 28 | topic_struct: z.array(TopicStructSchema), 29 | url_struct: z.array(z.any()), 30 | }) 31 | 32 | export type RawLongText = z.infer 33 | -------------------------------------------------------------------------------- /packages/core/src/types/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod' 2 | 3 | export type UID = string 4 | 5 | export const userSchema = z.object({ 6 | uid: z.string(), 7 | name: z.string(), 8 | avatar: z.string(), 9 | remark: z.string().optional(), 10 | }) 11 | 12 | const userWithBioSchema = userSchema.extend({ 13 | bio: z.string(), 14 | }) 15 | 16 | const userInfoSchema = userWithBioSchema.extend({ 17 | followers: z.number(), 18 | followings: z.number(), 19 | createdAt: z.string().optional(), 20 | birthday: z.string().optional(), 21 | postCount: z.number().optional(), 22 | exportedAt: z.string().optional(), 23 | }) 24 | 25 | const followingSchema = userSchema.extend({ 26 | followBy: z.string(), 27 | followers: z.number(), 28 | followings: z.number(), 29 | bio: z.string(), 30 | location: z.string(), 31 | createdAt: z.string(), 32 | }) 33 | 34 | export type User = z.infer 35 | export type UserBio = z.infer 36 | export type UserInfo = z.infer 37 | export type Following = z.infer 38 | -------------------------------------------------------------------------------- /packages/core/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export function scrollToTop() { 2 | window.scrollTo({ 3 | top: 0, 4 | behavior: 'smooth', 5 | }) 6 | } 7 | 8 | export async function readFile(e: Event) { 9 | return new Promise((resolve, reject) => { 10 | const file = (e.target as HTMLInputElement).files?.[0] 11 | if (!file) { 12 | reject(new Error('No file selected')) 13 | return 14 | } 15 | 16 | const reader = new FileReader() 17 | reader.onload = (e) => { 18 | const data = e.target?.result as string 19 | resolve(data) 20 | } 21 | reader.readAsText(file) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export class WeiboError extends Error { 2 | constructor( 3 | message: string, 4 | code?: string, 5 | ) { 6 | super(message) 7 | this.name = 'WeiboError' 8 | this.cause = code 9 | if (Error.captureStackTrace) { 10 | Error.captureStackTrace(this, this.constructor) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/utils/export.ts: -------------------------------------------------------------------------------- 1 | import type { Favorite, Post, UserBio, UserInfo } from '@weibo-archiver/core' 2 | import { WeiboParser } from '@weibo-archiver/core' 3 | import fileSaver from 'file-saver' 4 | 5 | export async function exportData(data: { 6 | weibo: Post[] 7 | user: UserInfo | null 8 | followings: UserBio[] 9 | favorites: Favorite[] 10 | }) { 11 | console.log('Exporting posts count:', data.weibo.length) 12 | if (!data.user?.name) { 13 | console.warn('User info is not available') 14 | return false 15 | } 16 | 17 | const { name: username } = data.user 18 | 19 | const dataStr = JSON.stringify(data) 20 | const dataBlob = new Blob([dataStr], { type: 'application/json' }) 21 | fileSaver.saveAs(dataBlob, `weibo-data-${username}.json`) 22 | 23 | const imgsData = [data.weibo, data.favorites] 24 | .flatMap(WeiboParser.parseImgs) 25 | .join(',\n') // csv 格式 26 | 27 | if (imgsData.length) { 28 | const imgsDataBlob = new Blob([imgsData], { type: 'text/csv' }) 29 | fileSaver.saveAs(imgsDataBlob, `imgs-${username}.csv`) 30 | } 31 | return true 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { CreateAxiosDefaults } from 'axios' 2 | import axios from 'axios' 3 | import { WEIBO_BASE_URL } from '../constants' 4 | import { WeiboError } from './error' 5 | 6 | export function createFetcher(args: CreateAxiosDefaults) { 7 | const _fetcher = axios.create({ 8 | ...args, 9 | baseURL: WEIBO_BASE_URL, 10 | }) 11 | 12 | return async function fetcher< 13 | T = any, 14 | R extends Record = Record, 15 | >( 16 | path: string, 17 | params?: R, 18 | ): Promise<{ data: T }> { 19 | return _fetcher(path, { params }).then(({ data: rawData, request }) => { 20 | const url = request.res?.responseUrl || path 21 | try { 22 | if (typeof rawData !== 'object') { 23 | throw new SyntaxError('Not a JSON') 24 | } 25 | 26 | const { ok, data, ...restData } = rawData || {} 27 | if (ok !== 1) { 28 | throw new WeiboError(`成功码不为 1: ${ok}`) 29 | } 30 | if (!data && restData) { 31 | return { 32 | data: restData, 33 | } 34 | } 35 | return rawData 36 | } 37 | catch (err: any) { 38 | if (err.name === `SyntaxError`) { 39 | throw new WeiboError(`未获取到 JSON,Cookie 可能已过期 [${url}]`) 40 | } 41 | throw new WeiboError(`获取失败:${err.message} [${url}]`) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | export type Fetcher = ReturnType 48 | -------------------------------------------------------------------------------- /packages/core/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Format the date string 3 | * @param time the date string 4 | * @param fmt the format string, e.g. `YYYY-MM-DD HH:mm:ss` 5 | */ 6 | export function formatDate( 7 | time: string | number | Date, 8 | fmt = 'YYYY-MM-DD HH:mm:ss', 9 | ) { 10 | if (typeof time === 'number' && time < 1e12) 11 | time *= 1000 12 | 13 | const date = new Date(time) 14 | if (Number.isNaN(date.getTime())) 15 | return '' 16 | 17 | const pad = (num: number) => num.toString().padStart(2, '0') 18 | 19 | const year = date.getFullYear() 20 | const month = pad(date.getMonth() + 1) // Months are zero-based 21 | const day = pad(date.getDate()) 22 | const hours = pad(date.getHours()) 23 | const minutes = pad(date.getMinutes()) 24 | const seconds = pad(date.getSeconds()) 25 | 26 | return fmt 27 | .replace('YYYY', year.toString()) 28 | .replace('MM', month) 29 | .replace('DD', day) 30 | .replace('HH', hours) 31 | .replace('mm', minutes) 32 | .replace('ss', seconds) 33 | } 34 | 35 | /** 36 | * Format the number 37 | * @param num the number 38 | * @param precision the precision 39 | */ 40 | export function formatNumber(num: number, precision = 2) { 41 | const wan = 1_0000 42 | const yi = 1_0000_0000 43 | 44 | if (num < wan) 45 | return `${num}` 46 | else if (num < yi) 47 | return `${(num / wan).toFixed(precision)}万` 48 | else 49 | return `${(num / yi).toFixed(precision)}亿` 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dom' 2 | export * from './emitter' 3 | export * from './error' 4 | export * from './export' 5 | export * from './fetch' 6 | export * from './format' 7 | export * from './IndexedDB' 8 | export * from './pqueue' 9 | 10 | export function delay(ms = 2000) { 11 | const randomMs = Math.random() * ms 12 | return new Promise(resolve => setTimeout(resolve, randomMs)) 13 | } 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'apps/*' 4 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Weibo-archiver 本地服务器 2 | 3 | 这是 Weibo-archiver 的本地服务程序,提供两个功能: 4 | 5 | 1. 下载微博中的图片,防止链接失效 6 | 2. 在本地运行一个服务器,用于离线浏览备份的微博数据和图片 7 | 8 | ## 使用方法 9 | 10 | 这是一个命令行程序,需要在终端中运行,或者直接双击运行按提示输入参数。 11 | 12 | ### 下载图片 13 | 14 | 从 imgs.csv 下载图片到当前目录的 images 文件夹内 15 | 16 | ```bash 17 | ./weibo-archiver.exe --dl -i imgs.csv -o images 18 | ``` 19 | 20 | 参数说明: 21 | 22 | ```bash 23 | 用法: main.exe [选项] 24 | 25 | 选项: 26 | -d, --dl 下载模式 27 | -s, --server 服务器模式 28 | -i, --imgs-path string imgs.csv 的路径 (default "imgs.csv") 29 | -o, --download-folder string 图片保存的文件夹 (default "images") 30 | -c, --concurrency int 同时下载的最大数量 (default 4) 31 | -t, --delay int 每次下载的间隔时间(秒) 32 | -h, --help 显示帮助信息 33 | ``` 34 | 35 | ### 启动本地服务 36 | 37 | 启动后访问 `http://localhost:3000` 即可浏览备份的微博数据。 38 | 39 | ```bash 40 | ./weibo-archiver.exe --server -o images 41 | ``` 42 | 43 | ## 注意事项 44 | 45 | 1. 下载的图片会按特定的规则重命名,然后被网页端识别 46 | 2. 本地服务支持离线浏览,下载后无需联网 47 | 48 | 如果实在不懂如何操作,可以把这份说明复制到 ChatGPT 等大语言模型中提问,并附上你的疑惑,或许可以得到答案。 49 | -------------------------------------------------------------------------------- /server/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "weibo-archiver/internal/config" 8 | "weibo-archiver/internal/ui" 9 | "weibo-archiver/internal/utils" 10 | ) 11 | 12 | func main() { 13 | info := utils.AppInfo{ 14 | Name: "Weibo-archiver Tools", 15 | Version: "0.6.0", 16 | Description: "Download and serve weibo images", 17 | } 18 | 19 | var opts config.Config 20 | 21 | if len(os.Args) < 2 { 22 | cfg, err := ui.RunInteractive() 23 | if err != nil { 24 | log.Fatalf("交互式配置失败: %v", err) 25 | } 26 | opts = *cfg 27 | } else { 28 | opts = utils.InitFlags(info) 29 | } 30 | 31 | if opts.IsExited { 32 | return 33 | } 34 | 35 | // 创建配置 36 | cfg := &config.Config{ 37 | Version: info.Version, 38 | Port: opts.Port, 39 | Concurrency: opts.Concurrency, 40 | DownloadDelay: opts.DownloadDelay, 41 | ImagesPath: opts.ImagesPath, 42 | CSVPath: opts.CSVPath, 43 | IsDownloadMode: opts.IsDownloadMode, 44 | IsServerMode: opts.IsServerMode, 45 | } 46 | 47 | fmt.Println("使用配置:") 48 | for k, v := range map[string]interface{}{ 49 | " 图片目录": cfg.ImagesPath, 50 | " CSV文件": cfg.CSVPath, 51 | " 端口": cfg.Port, 52 | " 并发数": cfg.Concurrency, 53 | " 下载延迟": cfg.DownloadDelay, 54 | } { 55 | fmt.Printf("%s: %v\n", k, v) 56 | } 57 | 58 | utils.Run(cfg) 59 | } 60 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module weibo-archiver 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.20.0 7 | github.com/charmbracelet/bubbletea v1.3.4 8 | github.com/charmbracelet/lipgloss v1.0.0 9 | github.com/schollz/progressbar/v3 v3.14.2 10 | github.com/spf13/pflag v1.0.5 11 | ) 12 | 13 | require ( 14 | github.com/atotto/clipboard v0.1.4 // indirect 15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 17 | github.com/charmbracelet/x/term v0.2.1 // indirect 18 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 19 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/mattn/go-localereader v0.0.1 // indirect 22 | github.com/mattn/go-runewidth v0.0.16 // indirect 23 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 24 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 25 | github.com/muesli/cancelreader v0.2.2 // indirect 26 | github.com/muesli/termenv v0.15.2 // indirect 27 | github.com/rivo/uniseg v0.4.7 // indirect 28 | github.com/sahilm/fuzzy v0.1.1 // indirect 29 | golang.org/x/sync v0.11.0 // indirect 30 | golang.org/x/sys v0.30.0 // indirect 31 | golang.org/x/term v0.17.0 // indirect 32 | golang.org/x/text v0.14.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /server/internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Config struct { 10 | Version string 11 | Port int 12 | Concurrency int 13 | DownloadDelay int 14 | ImagesPath string 15 | CSVPath string 16 | IsDownloadMode bool 17 | IsServerMode bool 18 | IsExited bool 19 | } 20 | 21 | // GetExecutableDir 获取可执行文件所在目录 22 | func GetExecutableDir() (string, error) { 23 | exe, err := os.Executable() 24 | if err != nil { 25 | return "", fmt.Errorf("获取可执行文件路径失败: %w", err) 26 | } 27 | return filepath.Dir(exe), nil 28 | } 29 | -------------------------------------------------------------------------------- /server/internal/server/images.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func (s *Server) newImageHandler() http.Handler { 12 | fileServer := http.FileServer(http.Dir(s.config.ImagesPath)) 13 | 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | // 设置缓存头 16 | w.Header().Set("Cache-Control", "public, max-age=3600") 17 | w.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat)) 18 | 19 | // 检查客户端是否支持gzip 20 | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 21 | fileServer.ServeHTTP(w, r) 22 | return 23 | } 24 | 25 | // 启用gzip压缩 26 | w.Header().Set("Content-Encoding", "gzip") 27 | gz := gzip.NewWriter(w) 28 | defer gz.Close() 29 | 30 | gzWriter := gzipResponseWriter{Writer: gz, ResponseWriter: w} 31 | fileServer.ServeHTTP(gzWriter, r) 32 | }) 33 | } 34 | 35 | type gzipResponseWriter struct { 36 | io.Writer 37 | http.ResponseWriter 38 | } 39 | 40 | func (w gzipResponseWriter) Write(b []byte) (int, error) { 41 | return w.Writer.Write(b) 42 | } -------------------------------------------------------------------------------- /server/internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "weibo-archiver/internal/config" 7 | ) 8 | 9 | type Server struct { 10 | config *config.Config 11 | mux *http.ServeMux 12 | } 13 | 14 | func New(cfg *config.Config) *Server { 15 | s := &Server{ 16 | config: cfg, 17 | mux: http.NewServeMux(), 18 | } 19 | 20 | // 注册路由 21 | s.registerRoutes() 22 | return s 23 | } 24 | 25 | func (s *Server) registerRoutes() { 26 | // 图片服务 27 | s.mux.Handle("/images/", http.StripPrefix("/images/", s.newImageHandler())) 28 | 29 | // SPA前端服务 30 | s.mux.Handle("/", s.newStaticHandler()) 31 | } 32 | 33 | func (s *Server) Start() error { 34 | port := s.config.Port 35 | if port == 0 { 36 | port = 3000 37 | } 38 | 39 | fmt.Printf("服务器已启动,图片文件夹: %s\n", s.config.ImagesPath) 40 | fmt.Printf("\t- 图片访问: http://localhost:%d/images/\n", port) 41 | fmt.Printf("\t- 网页访问: http://localhost:%d\n", port) 42 | 43 | return http.ListenAndServe(fmt.Sprintf(":%d", port), s.mux) 44 | } 45 | -------------------------------------------------------------------------------- /server/internal/server/static.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func (s *Server) newStaticHandler() http.Handler { 10 | webPath, _ := os.Getwd() 11 | 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | // 所有非资源请求都返回index.html 14 | if filepath.Ext(r.URL.Path) == "" { 15 | indexPath := filepath.Join(webPath, "index.html") 16 | if _, err := os.Stat(indexPath); err == nil { 17 | http.ServeFile(w, r, indexPath) 18 | return 19 | } 20 | } 21 | 22 | // 静态资源请求 23 | fileServer := http.FileServer(http.Dir(webPath)) 24 | fileServer.ServeHTTP(w, r) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /server/internal/ui/config.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | // Text 集中管理UI中使用的文本 8 | var Text = struct { 9 | AppTitle string 10 | DownloadMode string 11 | ServerMode string 12 | ConfigPrompt string 13 | ImageDirLabel string 14 | CSVFileLabel string 15 | ConfirmButton string 16 | HelpText string 17 | ModeSelection string 18 | DownloadDesc string 19 | ServerDesc string 20 | }{ 21 | AppTitle: "Weibo-archiver 本地工具", 22 | DownloadMode: "下载模式", 23 | ServerMode: "服务器模式", 24 | ConfigPrompt: "请配置%s参数:", 25 | ImageDirLabel: "图片目录", 26 | CSVFileLabel: "CSV文件路径", 27 | ConfirmButton: "[ 确认 ]", 28 | HelpText: "(使用方向键/tab 切换, enter 确认, ctrl+c 退出)", 29 | ModeSelection: "请选择运行模式", 30 | DownloadDesc: "从CSV文件下载微博图片", 31 | ServerDesc: "启动Web服务器浏览图片", 32 | } 33 | 34 | // Styles 集中管理UI中使用的样式 35 | var Styles = struct { 36 | Title lipgloss.Style 37 | Error lipgloss.Style 38 | Help lipgloss.Style 39 | Button lipgloss.Style 40 | }{ 41 | Title: lipgloss.NewStyle().Bold(true), 42 | Error: lipgloss.NewStyle().Foreground(lipgloss.Color("161")), 43 | Help: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), 44 | Button: lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")), 45 | } 46 | 47 | // DefaultValues 默认值 48 | var DefaultValues = struct { 49 | ImgDir string 50 | CSVFile string 51 | Concurrency int 52 | Delay int 53 | }{ 54 | ImgDir: "", 55 | CSVFile: "", 56 | Concurrency: 4, 57 | Delay: 0, 58 | } 59 | -------------------------------------------------------------------------------- /server/internal/ui/interactive.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "weibo-archiver/internal/config" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | // RunInteractive 运行交互式UI并返回用户选择的选项 10 | func RunInteractive() (*config.Config, error) { 11 | p := tea.NewProgram(NewModel()) 12 | 13 | teaModel, err := p.Run() 14 | 15 | model := teaModel.(Model) 16 | 17 | cfg := &config.Config{ 18 | ImagesPath: model.ImgDir, 19 | CSVPath: model.CsvFile, 20 | IsDownloadMode: model.IsDownload, 21 | IsServerMode: model.IsServer, 22 | IsExited: model.IsExited, 23 | Concurrency: 6, 24 | DownloadDelay: 1, 25 | } 26 | 27 | return cfg, err 28 | } 29 | -------------------------------------------------------------------------------- /server/internal/ui/renderer.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | 9 | // renderModeSelection 渲染模式选择界面 10 | func RenderModeSelection(m Model) string { 11 | return fmt.Sprintf("\n %s\n\n%s", 12 | Styles.Title.Render(Text.AppTitle), 13 | m.ModeList.View()) 14 | } 15 | 16 | // renderConfigForm 渲染配置表单界面 17 | func RenderConfigForm(m Model) string { 18 | var b strings.Builder 19 | 20 | // 标题 21 | modeTitle := Text.ServerMode 22 | if m.IsDownload { 23 | modeTitle = Text.DownloadMode 24 | } 25 | b.WriteString(fmt.Sprintf("\n %s - %s\n\n", 26 | Styles.Title.Render(Text.AppTitle), 27 | modeTitle)) 28 | 29 | // 提示文本 30 | b.WriteString(fmt.Sprintf(" %s\n\n", fmt.Sprintf(Text.ConfigPrompt, modeTitle))) 31 | 32 | // 图片目录输入框 33 | b.WriteString(fmt.Sprintf(" %s: %s\n\n", Text.ImageDirLabel, m.Inputs[0].View())) 34 | 35 | // CSV文件输入框 (仅下载模式) 36 | if m.IsDownload { 37 | b.WriteString(fmt.Sprintf(" %s: %s\n\n", Text.CSVFileLabel, m.Inputs[1].View())) 38 | } 39 | 40 | // 确认按钮 41 | button := Text.ConfirmButton 42 | if m.CurInput == len(m.Inputs) { 43 | button = Styles.Button.Render(button) 44 | } 45 | b.WriteString(fmt.Sprintf("\n %s\n\n", button)) 46 | 47 | // 错误信息 48 | if m.Error != nil { 49 | b.WriteString(Styles.Error.Render(fmt.Sprintf(" 错误: %s\n\n", m.Error))) 50 | } 51 | 52 | // 帮助文本 53 | b.WriteString(Styles.Help.Render(fmt.Sprintf(" %s\n", Text.HelpText))) 54 | 55 | return b.String() 56 | } 57 | -------------------------------------------------------------------------------- /server/internal/ui/validator.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "weibo-archiver/internal/utils" 7 | ) 8 | 9 | func CheckCSVFile(m Model) string { 10 | errorMsg := "" 11 | if m.CsvFile == "" { 12 | errorMsg = fmt.Sprintf("%s不能为空", Text.CSVFileLabel) 13 | } 14 | 15 | if !utils.FileExists(m.CsvFile) { 16 | errorMsg = fmt.Sprintf("文件不存在: %s", m.CsvFile) 17 | } 18 | 19 | if !strings.HasSuffix(strings.ToLower(m.CsvFile), ".csv") { 20 | errorMsg = fmt.Sprintf("%s 不是CSV文件", m.CsvFile) 21 | } 22 | 23 | return errorMsg 24 | } 25 | 26 | func CheckImageDir(m Model) string { 27 | errorMsg := "" 28 | if m.ImgDir == "" { 29 | errorMsg = fmt.Sprintf("%s不能为空", Text.ImageDirLabel) 30 | } 31 | 32 | if !utils.FileExists(m.ImgDir) { 33 | errorMsg = fmt.Sprintf("文件夹不存在: %s", m.ImgDir) 34 | } 35 | 36 | if !utils.IsDir(m.ImgDir) { 37 | errorMsg = fmt.Sprintf("%s 不是文件夹", m.ImgDir) 38 | } 39 | 40 | return errorMsg 41 | } 42 | -------------------------------------------------------------------------------- /server/internal/utils/argv.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "weibo-archiver/internal/config" 9 | 10 | "github.com/spf13/pflag" 11 | ) 12 | 13 | type AppInfo struct { 14 | Name string 15 | Version string 16 | Description string 17 | } 18 | 19 | func InitFlags(info AppInfo) config.Config { 20 | // 定义命令行参数 21 | opts := config.Config{} 22 | pflag.BoolVarP(&opts.IsDownloadMode, "dl", "d", false, "下载模式") 23 | pflag.BoolVarP(&opts.IsServerMode, "server", "s", false, "服务器模式") 24 | pflag.StringVarP(&opts.CSVPath, "imgs-path", "i", "imgs.csv", "imgs.csv 的路径") 25 | pflag.StringVarP(&opts.ImagesPath, "download-folder", "o", "images", "图片保存的文件夹") 26 | pflag.IntVarP(&opts.Concurrency, "concurrency", "c", 4, "同时下载的最大数量") 27 | pflag.IntVarP(&opts.DownloadDelay, "delay", "t", 0, "每次下载的间隔时间(秒)") 28 | help := pflag.BoolP("help", "h", false, "显示帮助信息") 29 | 30 | pflag.Parse() 31 | 32 | // 显示帮助信息 33 | if *help { 34 | exeName := filepath.Base(os.Args[0]) 35 | 36 | fmt.Printf("%s v%s - %s\n\n", info.Name, info.Version, info.Description) 37 | fmt.Printf("用法: %s [选项]\n\n", exeName) 38 | fmt.Println("选项:") 39 | pflag.PrintDefaults() 40 | 41 | fmt.Println("\n示例:") 42 | fmt.Println("\t" + exeName + " --dl -i imgs.csv -o images") 43 | fmt.Println("\t" + exeName + " --server -o images") 44 | fmt.Println("\t" + exeName + " (无参数,将进入交互模式)") 45 | os.Exit(0) 46 | } 47 | 48 | // 检查运行模式 49 | if opts.IsDownloadMode && opts.IsServerMode { 50 | fmt.Println("错误: 不能同时运行下载模式和服务器模式") 51 | os.Exit(1) 52 | } 53 | 54 | // 解析路径 55 | opts.CSVPath, _ = filepath.Abs(opts.CSVPath) 56 | opts.ImagesPath, _ = filepath.Abs(opts.ImagesPath) 57 | 58 | return opts 59 | } 60 | -------------------------------------------------------------------------------- /server/internal/utils/comom.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "time" 6 | "weibo-archiver/internal/config" 7 | "weibo-archiver/internal/server" 8 | ) 9 | 10 | func Run(cfg *config.Config) { 11 | if cfg.IsDownloadMode { 12 | dl := NewDownloader(cfg) 13 | if err := dl.Start(); err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | // 等2秒再退出 18 | time.Sleep(2 * time.Second) 19 | } else { 20 | srv := server.New(cfg) 21 | if err := srv.Start(); err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/internal/utils/files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | 7 | ) 8 | 9 | func ReadCSV(path string) ([]string, error) { 10 | file, err := os.Open(path) 11 | if err != nil { 12 | return nil, err 13 | } 14 | defer file.Close() 15 | 16 | scanner := bufio.NewScanner(file) 17 | urls := []string{} 18 | for scanner.Scan() { 19 | urls = append(urls, scanner.Text()) 20 | } 21 | 22 | return urls, nil 23 | } 24 | 25 | // fileExists 检查文件或目录是否存在 26 | func FileExists(path string) bool { 27 | _, err := os.Stat(path) 28 | return err == nil 29 | } 30 | 31 | // isDir 检查是否是目录 32 | func IsDir(path string) bool { 33 | stat, err := os.Stat(path) 34 | return err == nil && stat.IsDir() 35 | } 36 | -------------------------------------------------------------------------------- /server/internal/utils/queue.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type PQueue struct { 9 | concurrency int 10 | delay time.Duration 11 | tasks chan func() error 12 | wg sync.WaitGroup 13 | } 14 | 15 | func NewPQueue(concurrency int, delay time.Duration) *PQueue { 16 | if concurrency == 0 { 17 | concurrency = 6 18 | } 19 | if delay == 0 { 20 | delay = 1 * time.Second 21 | } 22 | q := &PQueue{ 23 | concurrency: concurrency, 24 | delay: delay, 25 | tasks: make(chan func() error, concurrency), 26 | } 27 | 28 | // Start worker goroutines 29 | for i := 0; i < concurrency; i++ { 30 | go q.worker() 31 | } 32 | 33 | return q 34 | } 35 | 36 | func (q *PQueue) worker() { 37 | for task := range q.tasks { 38 | if q.delay > 0 { 39 | time.Sleep(q.delay) 40 | } 41 | _ = task() 42 | q.wg.Done() 43 | } 44 | } 45 | 46 | func (q *PQueue) Add(task func() error) { 47 | q.wg.Add(1) 48 | q.tasks <- task 49 | } 50 | 51 | func (q *PQueue) Wait() { 52 | q.wg.Wait() 53 | } 54 | 55 | func (q *PQueue) Close() { 56 | close(q.tasks) 57 | } 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "jsxImportSource": "vue", 7 | "lib": ["DOM", "ESNext"], 8 | "useDefineForClassFields": true, 9 | "baseUrl": ".", 10 | "module": "ESNext", 11 | "moduleResolution": "Bundler", 12 | "paths": { 13 | "@weibo-archiver/core": ["packages/core"] 14 | }, 15 | "types": [ 16 | "vite/client" 17 | ], 18 | "allowJs": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "noUnusedLocals": true, 22 | "esModuleInterop": true, 23 | "skipLibCheck": true 24 | }, 25 | "include": [ 26 | "apps", 27 | "packages", 28 | "*.ts", 29 | "*.tsx", 30 | "*.d.ts", 31 | "apps/web/.nuxt/imports.d.ts", 32 | "eslint.config.js" 33 | ], 34 | "exclude": [ 35 | "dist", 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ], 8 | "redirects": [ 9 | { 10 | "source": "/docs", 11 | "destination": "https://docs.qq.com/doc/DTWttbXlMUGxZZnZq", 12 | "statusCode": 301 13 | } 14 | ] 15 | } 16 | --------------------------------------------------------------------------------
14 | 开始备份您的微博记忆,防止珍贵内容丢失 15 |
15 | 16 |
34 | {{ desc }} 35 |