├── .husky └── pre-commit ├── pnpm-workspace.yaml ├── packages ├── bilibili-bangumi-component │ ├── src │ │ ├── index.ts │ │ ├── components │ │ │ ├── Empty.tsx │ │ │ ├── Error.tsx │ │ │ ├── Skeleton.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── Pagination.tsx │ │ │ ├── List.tsx │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── shared │ │ │ ├── typeUtils.ts │ │ │ ├── dataMap.ts │ │ │ ├── api.ts │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ └── custom.ts │ │ ├── test │ │ │ └── utils.test.ts │ │ ├── components.d.ts │ │ └── index.html │ ├── .gitignore │ ├── README.md │ ├── stencil.config.ts │ ├── tsconfig.json │ ├── LICENSE │ └── package.json └── api │ ├── tsconfig.json │ ├── index.d.ts │ ├── src │ ├── shared │ │ ├── index.ts │ │ └── utils.ts │ ├── val-town.ts │ ├── cloudflare.ts │ ├── vercel.ts │ ├── mock │ │ ├── custom.js │ │ └── bili-bgm.js │ ├── bilibili.ts │ └── bgm.ts │ ├── package.json │ └── tsup.config.ts ├── docs ├── images │ ├── copy-code.png │ ├── http-handler.png │ ├── val-town-env.png │ ├── vercel │ │ ├── image.png │ │ ├── image-1.png │ │ ├── image-2.png │ │ ├── image-3.png │ │ ├── image-4.png │ │ ├── image-5.png │ │ ├── image-6.png │ │ ├── image-7.png │ │ ├── image-8.png │ │ ├── image-9.png │ │ └── configure.png │ ├── custom-preview.png │ ├── screenshot-pc.png │ ├── custom-val-town.png │ └── screenshot-mobile.png ├── custom.md ├── backend.md └── usage.md ├── .gitignore ├── .eslintignore ├── vercel.json ├── eslint.config.js ├── tsconfig.json ├── note.md ├── .vscode └── settings.json ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── LICENSE ├── package.json ├── README.md ├── public └── index.html └── api ├── val-town.js ├── cloudflare.js └── vercel.js /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run pre-commit 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - demos/* 4 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Components } from './components' 2 | -------------------------------------------------------------------------------- /docs/images/copy-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/copy-code.png -------------------------------------------------------------------------------- /docs/images/http-handler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/http-handler.png -------------------------------------------------------------------------------- /docs/images/val-town-env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/val-town-env.png -------------------------------------------------------------------------------- /docs/images/vercel/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image.png -------------------------------------------------------------------------------- /docs/images/custom-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/custom-preview.png -------------------------------------------------------------------------------- /docs/images/screenshot-pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/screenshot-pc.png -------------------------------------------------------------------------------- /docs/images/vercel/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-1.png -------------------------------------------------------------------------------- /docs/images/vercel/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-2.png -------------------------------------------------------------------------------- /docs/images/vercel/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-3.png -------------------------------------------------------------------------------- /docs/images/vercel/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-4.png -------------------------------------------------------------------------------- /docs/images/vercel/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-5.png -------------------------------------------------------------------------------- /docs/images/vercel/image-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-6.png -------------------------------------------------------------------------------- /docs/images/vercel/image-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-7.png -------------------------------------------------------------------------------- /docs/images/vercel/image-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-8.png -------------------------------------------------------------------------------- /docs/images/vercel/image-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/image-9.png -------------------------------------------------------------------------------- /docs/images/custom-val-town.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/custom-val-town.png -------------------------------------------------------------------------------- /docs/images/screenshot-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/screenshot-mobile.png -------------------------------------------------------------------------------- /docs/images/vercel/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yixiaojiu/bilibili-bangumi-component/HEAD/docs/images/vercel/configure.png -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["./index.d.ts", "node"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | 4 | **/.stencil/** 5 | **/dist/**' 6 | **/loader/** 7 | **/.stencil/** 8 | **/www/** 9 | 10 | .env*.local 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | 4 | **/.stencil/** 5 | **/dist/**' 6 | **/loader/** 7 | **/.stencil/** 8 | **/www/** 9 | 10 | api/* 11 | .env*.local 12 | 13 | -------------------------------------------------------------------------------- /packages/api/index.d.ts: -------------------------------------------------------------------------------- 1 | const isMock: boolean 2 | 3 | const mockDataMap = { 4 | bilibili: any, 5 | bgm: any, 6 | } 7 | 8 | const env = { 9 | BILIBILI: string, 10 | BGM: string, 11 | } 12 | 13 | const customData: any 14 | -------------------------------------------------------------------------------- /packages/api/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../../bilibili-bangumi-component/src/shared/types' 2 | export * from '../../../bilibili-bangumi-component/src/shared/utils' 3 | export * from '../../../bilibili-bangumi-component/src/shared/custom' 4 | -------------------------------------------------------------------------------- /packages/api/src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export function handleQuery(query: { pageNumber?: number, pageSize?: number }) { 2 | const { pageNumber = 1, pageSize = 10 } = query 3 | return { 4 | ...query, 5 | pageNumber: Number(pageNumber), 6 | pageSize: Number(pageSize), 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/(.*)", 5 | "headers": [ 6 | { 7 | "key": "Access-Control-Allow-Origin", 8 | "value": "*" 9 | } 10 | ] 11 | } 12 | ], 13 | "rewrites": [{ "source": "/api/(.*)", "destination": "/api/vercel" }], 14 | "framework": null 15 | } 16 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core' 2 | 3 | export function Empty() { 4 | return ( 5 |
6 | {/* @ts-expect-error */} 7 | empty 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | www/ 3 | loader/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.lock 9 | *.tmp 10 | *.tmp.* 11 | log.txt 12 | *.sublime-project 13 | *.sublime-workspace 14 | 15 | .stencil/ 16 | .idea/ 17 | .vscode/ 18 | .sass-cache/ 19 | .versions/ 20 | node_modules/ 21 | $RECYCLE.BIN/ 22 | 23 | .DS_Store 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | .env 27 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | // ignores: ['**/node_modules/**', '**/dist/**', '**/loader/**', '**/.stencil/**', '**/www/**'], 5 | rules: { 6 | 'node/prefer-global/process': 'off', 7 | 'unused-imports/no-unused-imports': 'off', 8 | 'ts/ban-ts-comment': 'off', 9 | }, 10 | gitignore: { 11 | files: ['.eslintignore'], 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "description": "", 7 | "author": "", 8 | "license": "ISC", 9 | "keywords": [], 10 | "main": "index.js", 11 | "scripts": { 12 | "dev": "MOCK=true && tsup --watch", 13 | "build": "tsup", 14 | "build:mock": "tsup --env.MOCK" 15 | }, 16 | "devDependencies": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "baseUrl": ".", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "noEmit": false, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/README.md: -------------------------------------------------------------------------------- 1 | # bilibili-bangumi-component 2 | 3 | 使用 [WebComponent](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components) 实现的追番列表组件,支持 Bilibili 与 Bangumi 4 | 5 | ## 特性 6 | 7 | - 💡 使用 WebComponent 实现,可用于任何前端应用 8 | - 🖼️ 支持 Bilibili 与 Bangumi 9 | - 🎨 支持主题设置 10 | - 🔌 支持自定义数据 11 | - 💪 适配移动端 12 | 13 | 详细介绍请参考 [github bilibili-bangumi-component](https://github.com/yixiaojiu/bilibili-bangumi-component) 文档 14 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/stencil.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@stencil/core' 2 | 3 | export const config: Config = { 4 | namespace: 'bilibili-bangumi-component', 5 | extras: { 6 | enableImportInjection: true, 7 | }, 8 | outputTargets: [ 9 | { 10 | type: 'dist', 11 | esmLoaderPath: '../loader', 12 | }, 13 | { 14 | type: 'www', 15 | serviceWorker: null, // disable service workers 16 | }, 17 | ], 18 | sourceMap: false, 19 | } 20 | -------------------------------------------------------------------------------- /note.md: -------------------------------------------------------------------------------- 1 | ## 参考 2 | 3 | https://roozen.top/bangumis 4 | 5 | https://demo.hclonely.com/bangumis/ 6 | 7 | https://blog.hans362.cn/bangumi/ 8 | 9 | https://github.com/hans362/Bilibili-Bangumi-JS 10 | 11 | https://github.com/HCLonely/hexo-bilibili-bangumi 12 | 13 | ## Api 14 | 15 | Bangumi: https://bangumi.github.io/api/#/model-SubjectType 16 | 17 | ## 其他 18 | WebComponent Compiler https://vue-quarkd.hellobike.com/#/ 19 | 20 | ## 图 21 | 22 | 空 https://s1.hdslb.com/bfs/static/webssr/article/empty.png https://www.bilibili.com/read/error 23 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/typeUtils.ts: -------------------------------------------------------------------------------- 1 | type UnionToIntersection = ( 2 | U extends any 3 | ? (x: U) => any 4 | : never 5 | ) extends (x: infer R) => any 6 | ? R 7 | : never 8 | 9 | type LastUnion = UnionToIntersection< 10 | U extends any 11 | ? (x: U) => 0 12 | : never 13 | > extends (x: infer R) => 0 14 | ? R 15 | : never 16 | 17 | export type UnionToTuple< 18 | T, 19 | Last = LastUnion, 20 | > = [T] extends [never] 21 | ? [] 22 | : [...UnionToTuple>, Last] 23 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core' 2 | 3 | interface ErrorProps { 4 | error: Error 5 | } 6 | 7 | export function Error({ error }: ErrorProps) { 8 | return ( 9 |
10 | parse failed 16 |

Σ(o゚д゚oノ) 发生了一些错误

17 |

{`message: ${error.message}`}

18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core' 2 | 3 | export function Skeleton() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "jsx": "react", 5 | "jsxFactory": "h", 6 | "lib": [ 7 | "dom", 8 | "es2017" 9 | ], 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "allowUnreachableCode": false, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "declaration": false, 17 | "allowSyntheticDefaultImports": true 18 | }, 19 | "include": [ 20 | "src" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "src/test" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | // Disable the default formatter, use eslint instead 5 | "prettier.enable": false, 6 | "editor.formatOnSave": false, 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | // Enable eslint for all supported languages 13 | "eslint.validate": [ 14 | "javascript", 15 | "javascriptreact", 16 | "typescript", 17 | "typescriptreact", 18 | "vue", 19 | "html", 20 | "markdown", 21 | "json", 22 | "jsonc", 23 | "yaml" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: pnpm/action-setup@v2 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: lts/* 21 | cache: pnpm 22 | 23 | - name: Install 24 | run: pnpm install 25 | 26 | - name: Lint 27 | run: pnpm run lint 28 | 29 | - name: build component 30 | run: pnpm run build:lib 31 | 32 | - name: build api 33 | run: pnpm run build:api 34 | -------------------------------------------------------------------------------- /packages/api/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { readFileSync } from 'node:fs' 3 | import { defineConfig } from 'tsup' 4 | 5 | const __dirname = path.dirname(new URL(import.meta.url).pathname) 6 | 7 | const biliBgmMock = readFileSync(path.resolve(__dirname, './src/mock/bili-bgm.js'), 'utf-8') 8 | const customMock = readFileSync(path.resolve(__dirname, './src/mock/custom.js'), 'utf-8') 9 | 10 | export default defineConfig((options) => { 11 | return { 12 | entry: ['src/vercel.ts', 'src/val-town.ts', 'src/cloudflare.ts'], 13 | outDir: path.resolve(__dirname, '../../api'), 14 | splitting: false, 15 | minify: !options.watch, 16 | clean: true, 17 | format: 'esm', 18 | banner: { 19 | js: options.env?.MOCK 20 | ? `${biliBgmMock} 21 | ${customMock} 22 | const isMock = true 23 | ` 24 | : 'const isMock = false', 25 | }, 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { numberToZh, parseSearchParams, serializeSearchParams } from '../shared/utils' 3 | 4 | it('numberToZh', () => { 5 | expect(numberToZh(1234)).toBe('1234') 6 | 7 | expect(numberToZh(123555)).toBe('12.3万') 8 | expect(numberToZh(120000)).toBe('12万') 9 | 10 | expect(numberToZh(123456789)).toBe('1.2亿') 11 | expect(numberToZh(100000000)).toBe('1亿') 12 | }) 13 | 14 | it('parseSearchParams', () => { 15 | expect(parseSearchParams(new URL('http://localhost?a=1&b=2'))).toEqual({ a: '1', b: '2' }) 16 | expect(parseSearchParams(new URL('http://localhost?a=&b=2'))).toEqual({ b: '2' }) 17 | }) 18 | 19 | it('serializeSearchParams', () => { 20 | expect(serializeSearchParams({ a: '1', b: '2' })).toBe('a=1&b=2') 21 | expect(serializeSearchParams({ a: undefined, b: '2' })).toBe('b=2') 22 | expect(serializeSearchParams({ a: '1', b: null, c: '3' })).toBe('a=1&c=3') 23 | }) 24 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core' 2 | import type { ContainerState } from '../shared/types' 3 | 4 | interface TabProps { 5 | labels: T[] 6 | activeLabel: T 7 | containerState: ContainerState 8 | onChange: (index: T) => void 9 | } 10 | 11 | export function Tabs({ activeLabel, labels, containerState, onChange }: TabProps) { 12 | const handleClick = (label: T) => { 13 | if (activeLabel !== label) 14 | onChange(label) 15 | } 16 | 17 | return ( 18 |
19 | {labels.map(label => ( 20 |
handleClick(label)} 28 | > 29 | {label} 30 |
31 | ))} 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/src/val-town.ts: -------------------------------------------------------------------------------- 1 | import { parseSearchParams } from '../../bilibili-bangumi-component/src/shared/utils' 2 | import { handler as bilibili } from './bilibili' 3 | import { handler as bgm } from './bgm' 4 | import { handleQuery } from './shared/utils' 5 | import { customHandler } from './shared' 6 | 7 | export default async function (request: Request) { 8 | const url = new URL(request.url) 9 | const query = handleQuery(parseSearchParams(url)) 10 | 11 | let envVal = {} 12 | try { 13 | envVal = env 14 | } 15 | catch { 16 | 17 | } 18 | 19 | let customSource = {} 20 | try { 21 | customSource = customData 22 | } 23 | catch { 24 | 25 | } 26 | 27 | if (url.pathname.endsWith('bilibili')) 28 | return await bilibili(query, envVal) 29 | else if (url.pathname.endsWith('bgm')) 30 | return await bgm(query, envVal) 31 | else if (url.pathname.endsWith('custom')) 32 | return customHandler(query, customSource) 33 | 34 | return Response.json({ 35 | code: 404, 36 | message: 'not found', 37 | data: {}, 38 | }, { status: 404 }) 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | run_install: true 24 | 25 | # after pnpm 26 | - name: Use Node.js LTS 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: 'lts/*' 30 | registry-url: https://registry.npmjs.org/ 31 | cache: pnpm 32 | 33 | - run: pnpm run build:lib 34 | # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 35 | - run: pnpm run ci:publish 36 | env: 37 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 38 | NPM_CONFIG_PROVENANCE: true 39 | 40 | - run: npx changelogithub 41 | env: 42 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 43 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/dataMap.ts: -------------------------------------------------------------------------------- 1 | import type { AnimeCollection, BookCollection, CollectionLabel, CollectionType, GameCollection, Subject, SubjectType } from './types' 2 | 3 | export const animeCollectionMap: Record = { 4 | 全部: '0', 5 | 想看: '1', 6 | 在看: '2', 7 | 看过: '3', 8 | } 9 | 10 | export const gameCollectionMap: Record = { 11 | 全部: '0', 12 | 想玩: '1', 13 | 在玩: '2', 14 | 玩过: '3', 15 | } 16 | 17 | export const bookCollectionMap: Record = { 18 | 全部: '0', 19 | 想读: '1', 20 | 在读: '2', 21 | 读过: '3', 22 | } 23 | 24 | export const collectionMap = { 25 | 动画: animeCollectionMap, 26 | 游戏: gameCollectionMap, 27 | 书籍: bookCollectionMap, 28 | } 29 | 30 | export const subjectMap: Record = { 31 | 动画: '1', 32 | 游戏: '2', 33 | 书籍: '3', 34 | } 35 | 36 | export const collectionLabelMap: Record = { 37 | 动画: ['全部', '想看', '在看', '看过'], 38 | 游戏: ['全部', '想玩', '在玩', '玩过'], 39 | 书籍: ['全部', '想读', '在读', '读过'], 40 | } 41 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { h } from '@stencil/core' 2 | 3 | export type ChangeType = 'head' | 'prev' | 'next' | 'tail' 4 | 5 | interface PaginationProps { 6 | pageNumber: number 7 | totalPages: number 8 | onChange: (changeType: ChangeType) => void 9 | onInputChange: (event: Event) => void 10 | } 11 | 12 | export function Pagination({ pageNumber, totalPages, onChange, onInputChange }: PaginationProps) { 13 | return ( 14 |
15 | onChange('head')}>首页 16 | onChange('prev')}>上一页 17 | {`${pageNumber} / ${totalPages}`} 18 | onChange('next')}>下一页 19 | onChange('tail')}>尾页 20 |
21 | 跳至 22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 翊小久 yixiajiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilibili-bangumi-component", 3 | "version": "0.3.1", 4 | "description": "展示 bilibili 与 Bangumi 追番列表的 WebComponent 组件", 5 | "license": "MIT", 6 | "homepage": "https://github.com/yixiaojiu/bilibili-bangumi-component#readme", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/yixiaojiu/bilibili-bangumi-component" 10 | }, 11 | "keywords": ["Bilibili", "Bangumi", "WebComponent"], 12 | "main": "dist/index.cjs.js", 13 | "module": "dist/index.js", 14 | "es2015": "dist/esm/index.js", 15 | "es2017": "dist/esm/index.js", 16 | "unpkg": "dist/bilibili-bangumi-component/bilibili-bangumi-component.esm.js", 17 | "types": "dist/types/index.d.ts", 18 | "collection": "dist/collection/collection-manifest.json", 19 | "collection:main": "dist/collection/index.js", 20 | "files": [ 21 | "dist/", 22 | "loader/" 23 | ], 24 | "scripts": { 25 | "build": "stencil build", 26 | "dev": "stencil build --dev --watch --serve", 27 | "generate": "stencil generate", 28 | "ts-check": "tsc --noEmit" 29 | }, 30 | "devDependencies": { 31 | "@stencil/core": "^4.7.0", 32 | "@types/node": "^20.10.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/api/src/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import { parseSearchParams } from '../../bilibili-bangumi-component/src/shared/utils' 2 | import { handler as bilibili } from './bilibili' 3 | import { handler as bgm } from './bgm' 4 | import { handleQuery } from './shared/utils' 5 | import { customHandler } from './shared' 6 | 7 | function setCORS(res: Response) { 8 | res.headers.set('Access-Control-Allow-Origin', '*') 9 | res.headers.set('Access-Control-Max-Age', '86400') 10 | return res 11 | } 12 | 13 | export default { 14 | async fetch(request: Request, env: NodeJS.ProcessEnv) { 15 | const url = new URL(request.url) 16 | const query = handleQuery(parseSearchParams(url)) 17 | 18 | let customSource = {} 19 | try { 20 | customSource = customData 21 | } 22 | catch { 23 | 24 | } 25 | 26 | if (url.pathname.endsWith('bilibili')) 27 | return setCORS(await bilibili(query, env)) 28 | else if (url.pathname.endsWith('bgm')) 29 | return setCORS(await bgm(query, env)) 30 | else if (url.pathname.endsWith('custom')) 31 | return setCORS(customHandler(query, customSource)) 32 | 33 | return Response.json({ 34 | code: 404, 35 | message: 'not found', 36 | data: {}, 37 | }, { status: 404 }) 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /packages/api/src/vercel.ts: -------------------------------------------------------------------------------- 1 | import { parseSearchParams } from '../../bilibili-bangumi-component/src/shared/utils' 2 | import { handler as bilibili } from './bilibili' 3 | import { handler as bgm } from './bgm' 4 | import { handleQuery } from './shared/utils' 5 | import { customHandler } from './shared' 6 | 7 | export const runtime = 'edge' 8 | export const preferredRegion = ['hkg1', 'hnd1', 'kix1', 'sin1'] 9 | 10 | export async function GET(request: Request) { 11 | const url = new URL(request.url) 12 | const query = handleQuery(parseSearchParams(url)) 13 | 14 | if (isMock) { 15 | if (url.pathname.endsWith('bilibili')) 16 | return Response.json(mockDataMap.bilibili) 17 | else if (url.pathname.endsWith('bgm')) 18 | return Response.json(mockDataMap.bgm) 19 | } 20 | 21 | let customSource = {} 22 | try { 23 | customSource = customData 24 | } 25 | catch { 26 | 27 | } 28 | 29 | if (url.pathname.endsWith('bilibili')) 30 | return await bilibili(query, process.env) 31 | else if (url.pathname.endsWith('bgm')) 32 | return await bgm(query, process.env) 33 | else if (url.pathname.endsWith('custom')) 34 | return customHandler(query, customSource) 35 | 36 | return Response.json({ 37 | code: 404, 38 | message: 'not found', 39 | data: {}, 40 | }, { status: 404 }) 41 | } 42 | -------------------------------------------------------------------------------- /packages/api/src/mock/custom.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | 3 | const customData = { 4 | // 动画 5 | anime: { 6 | // 想看 7 | want: [ 8 | // 在这里填入自定义的数据 9 | ], 10 | // 在看 11 | doing: [], 12 | // 看过 13 | done: [], 14 | }, 15 | // 游戏 16 | game: { 17 | want: [], 18 | doing: [ 19 | { 20 | // 名称 21 | name: '素晴らしき日々~不連続存在~', 22 | // 中文名 23 | nameCN: '美好的每一天 ~不连续的存在~', 24 | // 简介 25 | summary: '由6个故事所演奏的旋律――“话语与旋律”的故事\r\n\r\n“Down the Rabbit-Hole” ―― 「天空与世界」的故事\r\n“It’s my own Invention” ―― 「终结与开始」的故事\r\n“Looking-glass I', 26 | // 封面 27 | cover: 'https://lain.bgm.tv/pic/cover/l/33/b8/4639_kDq7d.jpg', 28 | // 链接 29 | url: 'https://bgm.tv/subject/4639', 30 | // 标签 31 | labels: [ 32 | { 33 | // 标签名 34 | label: '评分', 35 | // 标签值 36 | value: 8.9, 37 | }, 38 | { 39 | label: '排名', 40 | value: 15, 41 | }, 42 | { 43 | label: '时间', 44 | value: '2010-03-26', 45 | }, 46 | ], 47 | }, 48 | ], 49 | done: [], 50 | }, 51 | // 书籍 52 | book: { 53 | want: [], 54 | doing: [], 55 | done: [], 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /docs/custom.md: -------------------------------------------------------------------------------- 1 | # 自定义数据 2 | 3 | 效果: 4 | 5 | custom-val-town 6 | 7 | ## 后端 8 | 9 | 把数据定义在 JS 对象(与 JSON 的语法互通)中,然后放到 Serverless Function 的代码中,非常苯的方法 🥹 10 | 11 | 示例代码 [packages/api/src/mock/custom.js](/packages/api/src/mock/custom.js) 12 | 13 | 下面是 `customData` 的 ts 类型: 14 | 15 | ```ts 16 | interface CustomData { 17 | // 动画 18 | anime?: CollectionData 19 | // 游戏 20 | game?: CollectionData 21 | // 书籍 22 | book?: CollectionData 23 | } 24 | // 以动画为例:想看、在看、看过 25 | interface CollectionData { 26 | want?: ListItem[] 27 | doing?: ListItem[] 28 | done?: ListItem[] 29 | } 30 | interface ListItem { 31 | // 名称 32 | name?: string 33 | // 中文名 34 | nameCN?: string 35 | // 简介 36 | summary: string 37 | // 封面 38 | cover?: string 39 | // 链接 40 | url: string 41 | // 标签 42 | labels: LabelItem[] 43 | } 44 | interface LabelItem { 45 | // 标签名 46 | label: string 47 | // 标签值 48 | value?: string | number 49 | } 50 | ``` 51 | 52 | 例如放到 `Val Town ` 中,多余的字段可以删掉 53 | 54 | custom-val-town 55 | 56 | 由于 `vercel` 是通过 fork 本仓库进行部署的,因此需要把 `customData` 对象放到以下路径的代码中:[api/vercel.js](/api/vercel.js),然后提交更改。 57 | 58 | ## 前端 59 | 60 | 启用自定义数据源 61 | 62 | ```html 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/backend.md: -------------------------------------------------------------------------------- 1 | # 后端 2 | 3 | 后端使用 Serverless Function 实现 4 | 5 | ## 方案一:使用 val towm 6 | 7 | 1. 到 [val town](https://www.val.town/) 注册账号 8 | 9 | 2. 创建一个 HTTP handler 10 | ![HTTP handler](./images/http-handler.png) 11 | 12 | 3. 将 [api/val-town.js](api/val-town.js) 中的代码复制到此处 13 | 14 | ![copy-code](./images/copy-code.png) 15 | 16 | 4. (可选)添加 `uid` env 17 | 18 | ![val-town-env](./images/val-town-env.png) 19 | 20 | ## 方案二:使用 vercel 21 | 22 | **吐槽:** 一开始是以 vercel 的 [Edge Function](https://vercel.com/docs/functions/edge-functions) 为平台进行开发的,结果基本功能都开发完了,部署测试时发现 vercel 域名被墙了,气晕了 😡😡😡。 23 | 24 | 需要自己想办法解决 vercel 域名被墙的问题 25 | 26 | 1. fork 本项目,并在 vercel 中导入 27 | 28 | 2. 构建配置,并点击 Deploy 29 | 30 | ![vercel-configure](./images/vercel/configure.png) 31 | 32 | 3. 检查构建记录中是否注册了 Functions 33 | 34 | ![alt text](./images/vercel/image.png) 35 | 36 | ![alt text](./images/vercel/image-1.png) 37 | 38 | ![alt text](./images/vercel/image-2.png) 39 | 40 | 4. 如果构建记录中没有 Functions,则是部署资源的根目录出了问题 (如果有 Functions,直接跳过这一步) 41 | 42 | ![alt text](./images/vercel/image-3.png) 43 | 44 | ![alt text](./images/vercel/image-4.png) 45 | 46 | ![alt text](./images/vercel/image-5.png) 47 | 48 | ![alt text](./images/vercel/image-6.png) 49 | 50 | ![alt text](./images/vercel/image-7.png) 51 | 52 | 5. 验证是否部署成功 53 | 54 | ![alt text](./images/vercel/image-8.png) 55 | 56 | ![alt text](./images/vercel/image-9.png) 57 | 58 | **注意:** vercel 的接口需要加上 `/api` 路径,例如:`https://xxxx.vercel.app/api` 59 | 60 | ## 方案三:使用 cloudflare 61 | 62 | 与 val-town 类似,复制代码[cloudflare.js](/api/cloudflare.js) 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bilibili-bangumi-component/monorepo", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "packageManager": "pnpm@10.3.0", 7 | "description": "展示 bilibili 与 Bangumi 追番列表的 WebComponent 组件", 8 | "author": "yixiaojiu", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/yixiaojiu/bilibili-bangumi-component" 13 | }, 14 | "main": "index.js", 15 | "scripts": { 16 | "dev:vercel": "vercel dev", 17 | "dev:api": "pnpm --filter api run dev", 18 | "dev:lib": "pnpm --filter bilibili-bangumi-component run dev", 19 | "build:lib": "pnpm --filter bilibili-bangumi-component run build", 20 | "build:api": "pnpm --filter api run build", 21 | "build:mock": "pnpm --filter api run build:mock", 22 | "lint:fix": "eslint . --fix", 23 | "lint": "eslint .", 24 | "generate": "pnpm --filter bilibili-bangumi-component run generate", 25 | "test": "vitest run", 26 | "ts-check:lib": "pnpm --filter bilibili-bangumi-component run ts-check", 27 | "ci:publish": "pnpm -r publish --access public --no-git-checks", 28 | "pre-commit": "lint-staged --allow-empty", 29 | "prepare": "husky" 30 | }, 31 | "devDependencies": { 32 | "@antfu/eslint-config": "^2.6.3", 33 | "@types/node": "20", 34 | "@vercel/node": "^3.0.17", 35 | "eslint": "^8.56.0", 36 | "husky": "^9.0.7", 37 | "lint-staged": "^15.2.0", 38 | "tsup": "^8.0.1", 39 | "tsx": "^4.7.0", 40 | "typescript": "^5.3.3", 41 | "vercel": "^37.3.0", 42 | "vitest": "^1.2.1" 43 | }, 44 | "lint-staged": { 45 | "**/*.{tsx,ts,md,json}": [ 46 | "eslint . --fix" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/api.ts: -------------------------------------------------------------------------------- 1 | import { formatUrl, serializeSearchParams } from './utils' 2 | import type { BgmQuery, BilibiliQuery, Collection, Subject } from './types' 3 | import { animeCollectionMap, collectionMap, subjectMap } from './dataMap' 4 | 5 | export interface BilibiliParams { 6 | uid?: string 7 | collectionType: Collection 8 | pageSize: number 9 | pageNumber: number 10 | } 11 | 12 | interface BgmParams extends BilibiliParams { 13 | subjectType: Subject 14 | } 15 | 16 | type CustomParams = Omit 17 | 18 | export async function getBilibili(baseUrl: string, params: BilibiliParams) { 19 | const query: BilibiliQuery = { 20 | ...params, 21 | collectionType: animeCollectionMap[params.collectionType], 22 | } 23 | const res = await fetch(`${formatUrl(baseUrl)}/bilibili?${serializeSearchParams(query)}`) 24 | return await res.json() 25 | } 26 | 27 | export async function getBangumi(baseUrl: string, params: BgmParams) { 28 | const { subjectType } = params 29 | 30 | const query: BgmQuery = { 31 | ...params, 32 | collectionType: collectionMap[subjectType][params.collectionType], 33 | subjectType: subjectMap[params.subjectType], 34 | } 35 | const res = await fetch(`${formatUrl(baseUrl)}/bgm?${serializeSearchParams(query)}`) 36 | return await res.json() 37 | } 38 | 39 | export async function getCustom(baseUrl: string, params: CustomParams) { 40 | const { subjectType } = params 41 | 42 | const query: BgmQuery = { 43 | ...params, 44 | collectionType: collectionMap[subjectType][params.collectionType], 45 | subjectType: subjectMap[params.subjectType], 46 | } 47 | const res = await fetch(`${formatUrl(baseUrl)}/custom?${serializeSearchParams(query)}`) 48 | return await res.json() 49 | } 50 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /** 4 | * This is an autogenerated file created by the Stencil compiler. 5 | * It contains typing information for all components that exist in this project. 6 | */ 7 | import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; 8 | export namespace Components { 9 | interface BilibiliBangumi { 10 | "api": string; 11 | "bgmEnabled": boolean; 12 | "bgmUid"?: string; 13 | "bilibiliEnabled": boolean; 14 | "bilibiliUid"?: string; 15 | "customEnabled": boolean; 16 | "customLabel": string; 17 | "pageSize": number; 18 | } 19 | } 20 | declare global { 21 | interface HTMLBilibiliBangumiElement extends Components.BilibiliBangumi, HTMLStencilElement { 22 | } 23 | var HTMLBilibiliBangumiElement: { 24 | prototype: HTMLBilibiliBangumiElement; 25 | new (): HTMLBilibiliBangumiElement; 26 | }; 27 | interface HTMLElementTagNameMap { 28 | "bilibili-bangumi": HTMLBilibiliBangumiElement; 29 | } 30 | } 31 | declare namespace LocalJSX { 32 | interface BilibiliBangumi { 33 | "api"?: string; 34 | "bgmEnabled"?: boolean; 35 | "bgmUid"?: string; 36 | "bilibiliEnabled"?: boolean; 37 | "bilibiliUid"?: string; 38 | "customEnabled"?: boolean; 39 | "customLabel"?: string; 40 | "pageSize"?: number; 41 | } 42 | interface IntrinsicElements { 43 | "bilibili-bangumi": BilibiliBangumi; 44 | } 45 | } 46 | export { LocalJSX as JSX }; 47 | declare module "@stencil/core" { 48 | export namespace JSX { 49 | interface IntrinsicElements { 50 | "bilibili-bangumi": LocalJSX.BilibiliBangumi & JSXBase.HTMLAttributes; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilibili-bangumi-component 2 | 3 | 使用 [WebComponent](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components) 实现的追番列表组件,支持 Bilibili 与 Bangumi (目前支持动画、游戏与书籍) 4 | 5 | 参考 [hexo-bilibili-bangumi](https://github.com/HCLonely/hexo-bilibili-bangumi),[Bilibili-Bangumi-JS](https://github.com/hans362/Bilibili-Bangumi-JS),[Roozen的小破站](https://roozen.top/bangumis) 6 | 7 | ## 特性 8 | 9 | - 💡 使用 WebComponent 实现,可用于任何前端应用 10 | - 🖼️ 支持 Bilibili 与 Bangumi 11 | - 🎨 支持主题设置 12 | - 🔌 支持自定义数据 13 | - 💪 适配移动端 14 | 15 | ## 展示 16 | 17 | 展示地址 [note.yixiaojiu.top/docs/record/bangumi](https://note.yixiaojiu.top/docs/record/show-window/bangumi) 18 | 19 | screenshot-pc 20 | 21 | screenshot-mobile 22 | 23 | ## 文档 24 | 25 | 这里有视频教程 *⸜( •ᴗ• )⸝* [https://www.bilibili.com/video/BV1ht421W74u](https://www.bilibili.com/video/BV1ht421W74u) 26 | 27 | - 使用: [docs/usage.md](docs/usage.md) 28 | - 部署后端: [docs/backend.md](docs/backend.md) 29 | - 自定义数据源: [docs/custom.md](docs/custom.md) 30 | 31 | ## 第三方集成 32 | 33 | - Valaxy: [valaxy-addon-bangumi](https://github.com/YunYouJun/valaxy/tree/main/packages/valaxy-addon-bangumi) 34 | 35 | ## How to development 36 | 37 | 项目采用 monorepo,使用 pnpm 管理依赖,并在 `package.json` 指定了版本,版本对不上可能无法安装,可以把 `package.json` 里的限制删了 38 | 39 | ```sh 40 | pnpm i 41 | ``` 42 | 43 | ### server 44 | 45 | server 用的是 vercel 的服务,在本地开发时要关联 vercel 上的一个 project 46 | 47 | ```sh 48 | pnpm run build:api 49 | 50 | # 登陆并关联 vercel 51 | pnpm run dev:vercel 52 | ``` 53 | 54 | 仓库在 `./packages/api` 55 | 56 | 感觉比较麻烦可以直接改前端页面所用到的后端服务 `https://yi_xiao_jiu-bangumi.web.val.run`,文件位置在 `packages/bilibili-bangumi-component/src/index.html:60`, 57 | 58 | ### client 59 | 60 | ```sh 61 | pnpm run dev:lib 62 | ``` 63 | 64 | 仓库在 `./packages/bilibili-bangumi-component` 65 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseType } from './types' 2 | 3 | export function serializeSearchParams(searchParams: object) { 4 | return Object.entries(searchParams) 5 | .filter(([, value]) => !!value) 6 | .map(([key, value]) => `${key}=${value}`) 7 | .join('&') 8 | } 9 | 10 | export function parseSearchParams(url: URL) { 11 | return Object.fromEntries(Array.from(url.searchParams.entries()).filter(([, value]) => !!value)) 12 | } 13 | 14 | // 数字转中文 15 | export function numberToZh(num: number) { 16 | const numString = num.toString() 17 | if (numString.length < 5) 18 | return numString 19 | 20 | if (numString.length < 9) { 21 | const displayStr = numString.slice(0, -3) 22 | const length = displayStr.length 23 | const sub = displayStr[length - 1] === '0' ? '' : `.${displayStr[length - 1]}` 24 | return `${displayStr.slice(0, length - 1)}${sub}万` 25 | } 26 | 27 | const displayStr = numString.slice(0, -7) 28 | const length = displayStr.length 29 | const sub = displayStr[length - 1] === '0' ? '' : `.${displayStr[length - 1]}` 30 | return `${displayStr.slice(0, length - 1)}${sub}亿` 31 | } 32 | 33 | export function formatUrl(baseUrl: string) { 34 | if (!baseUrl.startsWith('http')) 35 | return baseUrl 36 | 37 | const url = new URL(baseUrl) 38 | const pathname = url.pathname === '/' ? '' : url.pathname 39 | return `${url.origin}${pathname}` 40 | } 41 | 42 | export function generateRes(params: ResponseType) { 43 | return Response.json(params, { 44 | status: params.code, 45 | }) 46 | } 47 | 48 | export function thorttle any>(fn: T, wait?: number) { 49 | let timer: any 50 | return function (...args: Parameters) { 51 | if (!timer) { 52 | timer = setTimeout(() => { 53 | timer = null 54 | fn.apply(this, args) 55 | }, wait) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import type { UnionToTuple } from './typeUtils' 2 | 3 | export type Platform = 'Bilibili' | 'Bangumi' | string 4 | 5 | export type Subject = '动画' | '游戏' | '书籍' 6 | 7 | export type AnimeCollection = '全部' | '想看' | '在看' | '看过' 8 | 9 | export type GameCollection = '全部' | '想玩' | '在玩' | '玩过' 10 | 11 | export type BookCollection = '全部' | '想读' | '在读' | '读过' 12 | 13 | export type Collection = AnimeCollection | GameCollection | BookCollection 14 | 15 | export type CollectionType = '0' | '1' | '2' | '3' 16 | 17 | export type CollectionLabel = UnionToTuple | UnionToTuple | UnionToTuple 18 | 19 | /** 20 | * 1 动画 2 游戏 3 书籍 21 | */ 22 | export type SubjectType = '1' | '2' | '3' 23 | 24 | export interface BilibiliQuery { 25 | /** 26 | * 筛选状态 27 | * 0 全部 28 | * 1 想看 29 | * 2 在看 30 | * 3 看过 31 | */ 32 | collectionType?: CollectionType 33 | 34 | uid?: string 35 | 36 | /** 37 | * 第几页 38 | */ 39 | pageNumber?: number 40 | 41 | /** 42 | * 一页多少个 43 | */ 44 | pageSize?: number 45 | } 46 | 47 | export interface BgmQuery extends BilibiliQuery { 48 | /** 49 | * 1 为动画 2 为游戏 3 为书籍 50 | */ 51 | subjectType?: SubjectType 52 | } 53 | 54 | export type CustomQuery = Omit 55 | 56 | interface EmptyData {} 57 | 58 | export interface ResponseType { 59 | code: number 60 | message: string 61 | data: ResponseData | EmptyData 62 | } 63 | 64 | export interface ResponseData { 65 | list: ListItem[] 66 | pageNumber: number 67 | pageSize: number 68 | total: number 69 | totalPages: number 70 | } 71 | 72 | export interface ListItem { 73 | // 原名 b站没有 74 | name?: string 75 | // 中文名 76 | nameCN?: string 77 | // 简介 78 | summary: string 79 | // 封面 80 | cover?: string 81 | // 资源链接 82 | url: string 83 | // 标签 84 | labels: LabelItem[] 85 | } 86 | 87 | export interface LabelItem { 88 | label: string 89 | value?: string | number 90 | } 91 | 92 | export type ContainerState = 'large' | 'middle' | 'small' 93 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/shared/custom.ts: -------------------------------------------------------------------------------- 1 | import type { CustomQuery, ListItem, SubjectType } from './types' 2 | import { generateRes } from './utils' 3 | 4 | export interface CustomData { 5 | anime?: CollectionData 6 | game?: CollectionData 7 | book?: CollectionData 8 | } 9 | 10 | /** 11 | * 以动画为例: want 想看, doing 在看, done 看过 12 | */ 13 | export interface CollectionData { 14 | want?: ListItem[] 15 | doing?: ListItem[] 16 | done?: ListItem[] 17 | } 18 | 19 | const customSubjectMap: Record = { 20 | 1: 'anime', 21 | 2: 'game', 22 | 3: 'book', 23 | } 24 | 25 | const customCollectionMap: Record = { 26 | 1: 'want', 27 | 2: 'doing', 28 | 3: 'done', 29 | } 30 | 31 | export function customHandler(params: CustomQuery, customData: CustomData): Response { 32 | const { subjectType = '1', collectionType = '0', pageNumber = 1, pageSize = 10 } = params 33 | 34 | const collectionData = customData[customSubjectMap[subjectType]] 35 | if (!collectionData) 36 | return generateEmpty(pageSize) 37 | 38 | let data: ListItem[] 39 | if (collectionType !== '0') { 40 | const list = collectionData[customCollectionMap[collectionType]] 41 | if (!list || !list.length) 42 | return generateEmpty(pageSize) 43 | data = list 44 | } 45 | else { 46 | const list = Object.values(collectionData).flat() 47 | if (!list.length) 48 | return generateEmpty(pageSize) 49 | data = list 50 | } 51 | 52 | return generateRes({ 53 | code: 200, 54 | message: 'ok', 55 | data: { 56 | list: data.slice((pageNumber - 1) * pageSize, pageNumber * pageSize), 57 | pageNumber, 58 | pageSize, 59 | total: data.length, 60 | totalPages: Math.ceil(data.length / pageSize), 61 | }, 62 | }) 63 | } 64 | 65 | function generateEmpty(pageSize: number): Response { 66 | return generateRes({ 67 | code: 200, 68 | message: 'ok', 69 | data: { 70 | list: [], 71 | pageNumber: 1, 72 | pageSize, 73 | total: 0, 74 | totalPages: 1, 75 | }, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from '@stencil/core' 2 | import { h } from '@stencil/core' 3 | import type { ContainerState, LabelItem, ListItem } from '../shared/types' 4 | import { Skeleton } from './Skeleton' 5 | 6 | interface LabelProps { 7 | containerState: ContainerState 8 | labels: LabelItem[] 9 | } 10 | 11 | const Label: FunctionalComponent = ({ containerState, labels }) => { 12 | let renderLabels = [...labels] 13 | 14 | if (containerState === 'middle') 15 | renderLabels = renderLabels.slice(0, 4) 16 | 17 | if (containerState === 'small') 18 | renderLabels = renderLabels.slice(0, 3) 19 | 20 | return ( 21 |
22 | {renderLabels.map(label => ( 23 |
28 |

{label.label}

29 | {label.value &&

{label.value}

} 30 |
31 | ))} 32 |
33 | ) 34 | } 35 | 36 | interface ListProps { 37 | loading: boolean 38 | list: ListItem[] 39 | containerState: ContainerState 40 | } 41 | 42 | export const List: FunctionalComponent = ({ list, loading, containerState }) => { 43 | return ( 44 |
45 | {list.map(item => ( 46 | 66 | ))} 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | bilibili-bangumi-component 8 | 12 | 13 | 65 | 66 | 67 | 68 | 71 |
72 |
73 | 74 |
75 |
76 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | bilibili-bangumi-component 7 | 8 | 9 | 10 | 53 | 54 | 55 | 58 |
59 |
60 | 61 |
62 |
63 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /packages/api/src/bilibili.ts: -------------------------------------------------------------------------------- 1 | import type { BilibiliQuery, LabelItem, ListItem, ResponseData } from '../../bilibili-bangumi-component/src/shared/types' 2 | import { numberToZh, serializeSearchParams } from '../../bilibili-bangumi-component/src/shared/utils' 3 | import { generateRes } from './shared' 4 | 5 | export async function handler(query: BilibiliQuery, env?: NodeJS.ProcessEnv) { 6 | const { collectionType = '0', uid: paramsUid, pageNumber = '1', pageSize = '10' } = query 7 | const vmid = paramsUid ?? env?.BILIBILI 8 | 9 | if (!vmid) { 10 | return generateRes({ 11 | code: 400, 12 | message: 'uid is required', 13 | data: {}, 14 | }) 15 | } 16 | const searchParams = serializeSearchParams({ 17 | type: 1, 18 | follow_status: collectionType, 19 | pn: pageNumber, 20 | ps: pageSize, 21 | vmid, 22 | }) 23 | 24 | const res = await fetch(`https://api.bilibili.com/x/space/bangumi/follow/list?${searchParams}`) 25 | const data = await res.json() 26 | 27 | if (!res.ok || data?.code !== 0) { 28 | return generateRes({ 29 | code: 502, 30 | message: data.message, 31 | data: {}, 32 | }) 33 | } 34 | 35 | return generateRes({ 36 | code: 200, 37 | message: 'ok', 38 | data: handleFetchData(data.data), 39 | }) 40 | } 41 | 42 | function handleFetchData(data: any): ResponseData { 43 | const list = (data?.list as any[])?.map((item) => { 44 | const labels: LabelItem[] = [ 45 | { 46 | label: item?.new_ep?.index_show, 47 | }, 48 | { 49 | label: '评分', 50 | value: item?.rating?.score, 51 | }, 52 | { 53 | label: '播放量', 54 | value: item?.stat?.view && numberToZh(item.stat.view), 55 | }, 56 | { 57 | label: '追番数', 58 | value: item?.stat?.follow && numberToZh(item.stat.follow), 59 | }, 60 | { 61 | label: '投币数', 62 | value: item?.stat?.coin && numberToZh(item.stat.coin), 63 | }, 64 | { 65 | label: '弹幕数', 66 | value: item?.stat?.danmaku && numberToZh(item.stat.danmaku), 67 | }, 68 | ] 69 | 70 | let cover = item.cover as string 71 | 72 | if (cover && cover.startsWith('http:')) { 73 | const url = new URL(cover) 74 | url.protocol = 'https:' 75 | cover = url.toString() 76 | } 77 | 78 | return { 79 | nameCN: item.title, 80 | summary: item.summary, 81 | cover, 82 | url: item.url, 83 | labels: labels.filter(item => item.label), 84 | } 85 | }) 86 | 87 | return { 88 | list: list ?? [], 89 | pageNumber: data.pn, 90 | pageSize: data.ps, 91 | total: data.total, 92 | totalPages: Math.ceil(data.total / data.ps), 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/api/src/bgm.ts: -------------------------------------------------------------------------------- 1 | import type { BgmQuery, LabelItem, ListItem, ResponseData, SubjectType } from '../../bilibili-bangumi-component/src/shared/types' 2 | import { serializeSearchParams } from '../../bilibili-bangumi-component/src/shared/utils' 3 | import { generateRes } from './shared' 4 | 5 | const subjectTypeMap: Record = { 6 | 1: '2', // 动画 7 | 2: '4', // 游戏 8 | 3: '1', // 书籍 9 | } 10 | 11 | const collectionTypeMap = { 12 | 0: null, // 全部 13 | 1: '1', // 想看 14 | 2: '3', // 在看 15 | 3: '2', // 看过 16 | } 17 | 18 | export async function handler(params: BgmQuery, env?: NodeJS.ProcessEnv) { 19 | const { subjectType = '1', uid: paramsUid, collectionType = '0', pageNumber = 1, pageSize = 10 } = params 20 | const uid = paramsUid ?? env?.BGM 21 | 22 | if (!uid) { 23 | return generateRes({ 24 | code: 400, 25 | message: `uid is required`, 26 | data: {}, 27 | }) 28 | } 29 | const searchParams = serializeSearchParams({ 30 | subject_type: subjectTypeMap[subjectType], 31 | type: collectionTypeMap[collectionType], 32 | limit: pageSize, 33 | offset: (Number(pageNumber) - 1) * Number(pageSize), 34 | }) 35 | 36 | const res = await fetch(`https://api.bgm.tv/v0/users/${uid}/collections?${searchParams}`, { 37 | headers: { 38 | 'User-Agent': `yixiaojiu/bilibili-bangumi-component (https://github.com/yixiaojiu/bilibili-bangumi-component)`, 39 | }, 40 | }) 41 | const data = await res.json() 42 | 43 | if (!res.ok) { 44 | return generateRes({ 45 | code: 502, 46 | message: data.description, 47 | data: {}, 48 | }) 49 | } 50 | 51 | return generateRes({ 52 | code: 200, 53 | message: 'ok', 54 | data: handleFetchData(data, { pageNumber: Number(pageNumber), pageSize: Number(pageSize) }), 55 | }) 56 | } 57 | 58 | function handleFetchData(data: any, init: { pageNumber: number, pageSize: number }): ResponseData { 59 | const list = (data?.data as any[])?.map((item) => { 60 | const subject = item?.subject 61 | const labels: LabelItem[] = [ 62 | { 63 | label: subject?.eps && `${subject.eps}话`, 64 | }, 65 | { 66 | label: '评分', 67 | value: subject?.score, 68 | }, 69 | { 70 | label: '排名', 71 | value: subject?.rank, 72 | }, 73 | { 74 | label: '时间', 75 | value: subject?.date, 76 | }, 77 | ] 78 | return { 79 | name: subject?.name, 80 | nameCN: subject?.name_cn, 81 | summary: subject?.short_summary, 82 | cover: subject?.images?.large, 83 | url: subject?.id ? `https://bgm.tv/subject/${subject?.id}` : 'https://bgm.tv/', 84 | labels: labels.filter((item) => { 85 | if ('value' in item) 86 | return item.value 87 | else 88 | return item.label 89 | }), 90 | } 91 | }) 92 | return { 93 | list: list ?? [], 94 | ...init, 95 | total: data.total, 96 | totalPages: Math.ceil(data.total / init.pageSize), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /api/val-town.js: -------------------------------------------------------------------------------- 1 | const isMock = false 2 | function p(t){return Object.entries(t).filter(([,r])=>!!r).map(([r,e])=>`${r}=${e}`).join("&")}function f(t){return Object.fromEntries(Array.from(t.searchParams.entries()).filter(([,r])=>!!r))}function m(t){let r=t.toString();if(r.length<5)return r;if(r.length<9){let s=r.slice(0,-3),n=s.length,i=s[n-1]==="0"?"":`.${s[n-1]}`;return`${s.slice(0,n-1)}${i}\u4E07`}let e=r.slice(0,-7),o=e.length,a=e[o-1]==="0"?"":`.${e[o-1]}`;return`${e.slice(0,o-1)}${a}\u4EBF`}function c(t){return Response.json(t,{status:t.code})}var v={1:"anime",2:"game",3:"book"},j={1:"want",2:"doing",3:"done"};function d(t,r){let{subjectType:e="1",collectionType:o="0",pageNumber:a=1,pageSize:s=10}=t,n=r[v[e]];if(!n)return b(s);let i;if(o!=="0"){let l=n[j[o]];if(!l||!l.length)return b(s);i=l}else{let l=Object.values(n).flat();if(!l.length)return b(s);i=l}return c({code:200,message:"ok",data:{list:i.slice((a-1)*s,a*s),pageNumber:a,pageSize:s,total:i.length,totalPages:Math.ceil(i.length/s)}})}function b(t){return c({code:200,message:"ok",data:{list:[],pageNumber:1,pageSize:t,total:0,totalPages:1}})}async function h(t,r){let{collectionType:e="0",uid:o,pageNumber:a="1",pageSize:s="10"}=t,n=o??r?.BILIBILI;if(!n)return c({code:400,message:"uid is required",data:{}});let i=p({type:1,follow_status:e,pn:a,ps:s,vmid:n}),l=await fetch(`https://api.bilibili.com/x/space/bangumi/follow/list?${i}`),u=await l.json();return!l.ok||u?.code!==0?c({code:502,message:u.message,data:{}}):c({code:200,message:"ok",data:R(u.data)})}function R(t){return{list:t?.list?.map(e=>{let o=[{label:e?.new_ep?.index_show},{label:"\u8BC4\u5206",value:e?.rating?.score},{label:"\u64AD\u653E\u91CF",value:e?.stat?.view&&m(e.stat.view)},{label:"\u8FFD\u756A\u6570",value:e?.stat?.follow&&m(e.stat.follow)},{label:"\u6295\u5E01\u6570",value:e?.stat?.coin&&m(e.stat.coin)},{label:"\u5F39\u5E55\u6570",value:e?.stat?.danmaku&&m(e.stat.danmaku)}],a=e.cover;if(a&&a.startsWith("http:")){let s=new URL(a);s.protocol="https:",a=s.toString()}return{nameCN:e.title,summary:e.summary,cover:a,url:e.url,labels:o.filter(s=>s.label)}})??[],pageNumber:t.pn,pageSize:t.ps,total:t.total,totalPages:Math.ceil(t.total/t.ps)}}var x={1:"2",2:"4",3:"1"},N={0:null,1:"1",2:"3",3:"2"};async function y(t,r){let{subjectType:e="1",uid:o,collectionType:a="0",pageNumber:s=1,pageSize:n=10}=t,i=o??r?.BGM;if(!i)return c({code:400,message:"uid is required",data:{}});let l=p({subject_type:x[e],type:N[a],limit:n,offset:(Number(s)-1)*Number(n)}),u=await fetch(`https://api.bgm.tv/v0/users/${i}/collections?${l}`,{headers:{"User-Agent":"yixiaojiu/bilibili-bangumi-component (https://github.com/yixiaojiu/bilibili-bangumi-component)"}}),g=await u.json();return u.ok?c({code:200,message:"ok",data:w(g,{pageNumber:Number(s),pageSize:Number(n)})}):c({code:502,message:g.description,data:{}})}function w(t,r){return{list:t?.data?.map(o=>{let a=o?.subject,s=[{label:a?.eps&&`${a.eps}\u8BDD`},{label:"\u8BC4\u5206",value:a?.score},{label:"\u6392\u540D",value:a?.rank},{label:"\u65F6\u95F4",value:a?.date}];return{name:a?.name,nameCN:a?.name_cn,summary:a?.short_summary,cover:a?.images?.large,url:a?.id?`https://bgm.tv/subject/${a?.id}`:"https://bgm.tv/",labels:s.filter(n=>"value"in n?n.value:n.label)}})??[],...r,total:t.total,totalPages:Math.ceil(t.total/r.pageSize)}}function S(t){let{pageNumber:r=1,pageSize:e=10}=t;return{...t,pageNumber:Number(r),pageSize:Number(e)}}async function F(t){let r=new URL(t.url),e=S(f(r)),o={};try{o=env}catch{}let a={};try{a=customData}catch{}return r.pathname.endsWith("bilibili")?await h(e,o):r.pathname.endsWith("bgm")?await y(e,o):r.pathname.endsWith("custom")?d(e,a):Response.json({code:404,message:"not found",data:{}},{status:404})}export{F as default}; 3 | -------------------------------------------------------------------------------- /api/cloudflare.js: -------------------------------------------------------------------------------- 1 | const isMock = false 2 | function p(e){return Object.entries(e).filter(([,s])=>!!s).map(([s,t])=>`${s}=${t}`).join("&")}function d(e){return Object.fromEntries(Array.from(e.searchParams.entries()).filter(([,s])=>!!s))}function m(e){let s=e.toString();if(s.length<5)return s;if(s.length<9){let r=s.slice(0,-3),n=r.length,i=r[n-1]==="0"?"":`.${r[n-1]}`;return`${r.slice(0,n-1)}${i}\u4E07`}let t=s.slice(0,-7),o=t.length,a=t[o-1]==="0"?"":`.${t[o-1]}`;return`${t.slice(0,o-1)}${a}\u4EBF`}function c(e){return Response.json(e,{status:e.code})}var j={1:"anime",2:"game",3:"book"},v={1:"want",2:"doing",3:"done"};function h(e,s){let{subjectType:t="1",collectionType:o="0",pageNumber:a=1,pageSize:r=10}=e,n=s[j[t]];if(!n)return b(r);let i;if(o!=="0"){let l=n[v[o]];if(!l||!l.length)return b(r);i=l}else{let l=Object.values(n).flat();if(!l.length)return b(r);i=l}return c({code:200,message:"ok",data:{list:i.slice((a-1)*r,a*r),pageNumber:a,pageSize:r,total:i.length,totalPages:Math.ceil(i.length/r)}})}function b(e){return c({code:200,message:"ok",data:{list:[],pageNumber:1,pageSize:e,total:0,totalPages:1}})}async function y(e,s){let{collectionType:t="0",uid:o,pageNumber:a="1",pageSize:r="10"}=e,n=o??s?.BILIBILI;if(!n)return c({code:400,message:"uid is required",data:{}});let i=p({type:1,follow_status:t,pn:a,ps:r,vmid:n}),l=await fetch(`https://api.bilibili.com/x/space/bangumi/follow/list?${i}`),u=await l.json();return!l.ok||u?.code!==0?c({code:502,message:u.message,data:{}}):c({code:200,message:"ok",data:x(u.data)})}function x(e){return{list:e?.list?.map(t=>{let o=[{label:t?.new_ep?.index_show},{label:"\u8BC4\u5206",value:t?.rating?.score},{label:"\u64AD\u653E\u91CF",value:t?.stat?.view&&m(t.stat.view)},{label:"\u8FFD\u756A\u6570",value:t?.stat?.follow&&m(t.stat.follow)},{label:"\u6295\u5E01\u6570",value:t?.stat?.coin&&m(t.stat.coin)},{label:"\u5F39\u5E55\u6570",value:t?.stat?.danmaku&&m(t.stat.danmaku)}],a=t.cover;if(a&&a.startsWith("http:")){let r=new URL(a);r.protocol="https:",a=r.toString()}return{nameCN:t.title,summary:t.summary,cover:a,url:t.url,labels:o.filter(r=>r.label)}})??[],pageNumber:e.pn,pageSize:e.ps,total:e.total,totalPages:Math.ceil(e.total/e.ps)}}var N={1:"2",2:"4",3:"1"},w={0:null,1:"1",2:"3",3:"2"};async function S(e,s){let{subjectType:t="1",uid:o,collectionType:a="0",pageNumber:r=1,pageSize:n=10}=e,i=o??s?.BGM;if(!i)return c({code:400,message:"uid is required",data:{}});let l=p({subject_type:N[t],type:w[a],limit:n,offset:(Number(r)-1)*Number(n)}),u=await fetch(`https://api.bgm.tv/v0/users/${i}/collections?${l}`,{headers:{"User-Agent":"yixiaojiu/bilibili-bangumi-component (https://github.com/yixiaojiu/bilibili-bangumi-component)"}}),f=await u.json();return u.ok?c({code:200,message:"ok",data:L(f,{pageNumber:Number(r),pageSize:Number(n)})}):c({code:502,message:f.description,data:{}})}function L(e,s){return{list:e?.data?.map(o=>{let a=o?.subject,r=[{label:a?.eps&&`${a.eps}\u8BDD`},{label:"\u8BC4\u5206",value:a?.score},{label:"\u6392\u540D",value:a?.rank},{label:"\u65F6\u95F4",value:a?.date}];return{name:a?.name,nameCN:a?.name_cn,summary:a?.short_summary,cover:a?.images?.large,url:a?.id?`https://bgm.tv/subject/${a?.id}`:"https://bgm.tv/",labels:r.filter(n=>"value"in n?n.value:n.label)}})??[],...s,total:e.total,totalPages:Math.ceil(e.total/s.pageSize)}}function R(e){let{pageNumber:s=1,pageSize:t=10}=e;return{...e,pageNumber:Number(s),pageSize:Number(t)}}function g(e){return e.headers.set("Access-Control-Allow-Origin","*"),e.headers.set("Access-Control-Max-Age","86400"),e}var F={async fetch(e,s){let t=new URL(e.url),o=R(d(t)),a={};try{a=customData}catch{}return t.pathname.endsWith("bilibili")?g(await y(o,s)):t.pathname.endsWith("bgm")?g(await S(o,s)):t.pathname.endsWith("custom")?g(h(o,a)):Response.json({code:404,message:"not found",data:{}},{status:404})}};export{F as default}; 3 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # 使用 2 | 3 | ## `uid` 获取 4 | 5 | 下面要用 6 | 7 | ### Bilibili 8 | 9 | 登录哔哩哔哩后前往 [https://space.bilibili.com/](https://space.bilibili.com)页面,网址最后的一串数字就是 `uid` 10 | 11 | **注意:** 追番信息属于个人隐私,默认是不公开的,需要在 b 站将追番设置为公开! 12 | 13 | ### Bangumi 14 | 15 | 登录 [Bangumi](https://bangumi.tv/) 后打开控制台(Ctrl+Shift+J),输入CHOBITS_UID回车,下面会输出 `uid` 16 | 17 | --- 18 | 19 | ## 后端 20 | 21 | 如果你不太方便搭建后端服务,可以先使用这个地址 `https://yi_xiao_jiu-bangumi.web.val.run` 22 | 23 | 部署后端,请查看 [docs/backend.md](./backend.md) 24 | 25 | --- 26 | 27 | ## 前端 28 | 29 | ### 引入 30 | 31 | 1. 使用 CDN 32 | 33 | ```html 34 | 38 | ``` 39 | 40 | 2. 使用包管理工具 41 | 42 | ```sh 43 | npm i bilibili-bangumi-component 44 | # or 45 | yarn add bilibili-bangumi-component 46 | # or 47 | pnpm add bilibili-bangumi-component 48 | ``` 49 | 50 | ### 使用组件 51 | 52 | 使用包管理工具引入需要先注册组件 53 | 54 | 在任意 js 代码中执行 55 | 56 | ```js 57 | import { defineCustomElements } from 'bilibili-bangumi-component/loader' 58 | 59 | // 注册 web component 组件 60 | defineCustomElements() 61 | ``` 62 | 63 | 在任意 html 中使用组件 64 | 65 | ```html 66 | 67 | 68 | ``` 69 | 70 | ### react 使用示例 71 | 72 | ```jsx 73 | import { defineCustomElements } from 'bilibili-bangumi-component/loader' 74 | 75 | // 注册 web component 组件 76 | defineCustomElements() 77 | 78 | export function Bangumi() { 79 | return ( 80 | 81 | ) 82 | } 83 | ``` 84 | 85 | ### 样式覆盖 86 | 87 | 由于使用了 [Shadow DOM](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_shadow_DOM),因此样式覆盖有一点点麻烦。 88 | 89 | 由于使用了 [@layer 级联层](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@layer),所以覆盖样式时不需要担心 CSS 优先级问题 90 | 91 | ```js 92 | const bilibiliBangumi = document.querySelector('bilibili-bangumi') 93 | const style = document.createElement('style') 94 | style.textContent = ` 95 | /* 编写需要覆盖的样式 */ 96 | 97 | .bbc-tab-item { 98 | color: #ccc; 99 | } 100 | ` 101 | bilibiliBangumi.shadowRoot.appendChild(style) 102 | ``` 103 | 104 | ### 主题 105 | 106 | 可以直接用标签选择器 `bilibili-bangumi` 进行覆盖 107 | 108 | ```css 109 | bilibili-bangumi { 110 | /* 基础文本颜色 */ 111 | --bbc-text-base-color: #4c4948; 112 | /* 标签颜色 */ 113 | --bbc-label-color: #FF9843; 114 | /* 下划线、背景之类的颜色 */ 115 | --bbc-primary-color: #425aef; 116 | } 117 | ``` 118 | 119 | ### 常见问题 120 | 121 | [https://vue-quarkd.hellobike.com/#/zh-CN/guide/notice](https://vue-quarkd.hellobike.com/#/zh-CN/guide/notice) 122 | 123 | ## API 124 | 125 | ### 组件 126 | 127 | | 字段 | 描述 | 类型 | 默认值 | 128 | |:--------------:|:----------------------------------------------------:|:------:|:------:| 129 | | api | 后端 api 地址 | string | - | 130 | | bilibili-uid | Bilibili 的 uid,在后端中引入 uid 的 env 后可以不设置 | string | - | 131 | | bgm-uid | Bangumi 的 uid,在后端中引入 uid 的 env 后可以不设置 | string | - | 132 | | bilibili-enabled | 是否展示 Bilibili | boolean | true | 133 | | bgm-enabled | 是否展示 Bangumi | boolean | true | 134 | | page-size | 分页大小 | number | 15 | 135 | | custom-enabled | 是否启动自定义数据源 | boolean | false | 136 | | custom-label | 自定义数据源的展示标签名 | string | "'自定义'" | 137 | -------------------------------------------------------------------------------- /api/vercel.js: -------------------------------------------------------------------------------- 1 | const isMock = false 2 | function p(t){return Object.entries(t).filter(([,a])=>!!a).map(([a,e])=>`${a}=${e}`).join("&")}function f(t){return Object.fromEntries(Array.from(t.searchParams.entries()).filter(([,a])=>!!a))}function m(t){let a=t.toString();if(a.length<5)return a;if(a.length<9){let r=a.slice(0,-3),n=r.length,i=r[n-1]==="0"?"":`.${r[n-1]}`;return`${r.slice(0,n-1)}${i}\u4E07`}let e=a.slice(0,-7),o=e.length,s=e[o-1]==="0"?"":`.${e[o-1]}`;return`${e.slice(0,o-1)}${s}\u4EBF`}function c(t){return Response.json(t,{status:t.code})}var j={1:"anime",2:"game",3:"book"},v={1:"want",2:"doing",3:"done"};function d(t,a){let{subjectType:e="1",collectionType:o="0",pageNumber:s=1,pageSize:r=10}=t,n=a[j[e]];if(!n)return b(r);let i;if(o!=="0"){let l=n[v[o]];if(!l||!l.length)return b(r);i=l}else{let l=Object.values(n).flat();if(!l.length)return b(r);i=l}return c({code:200,message:"ok",data:{list:i.slice((s-1)*r,s*r),pageNumber:s,pageSize:r,total:i.length,totalPages:Math.ceil(i.length/r)}})}function b(t){return c({code:200,message:"ok",data:{list:[],pageNumber:1,pageSize:t,total:0,totalPages:1}})}async function h(t,a){let{collectionType:e="0",uid:o,pageNumber:s="1",pageSize:r="10"}=t,n=o??a?.BILIBILI;if(!n)return c({code:400,message:"uid is required",data:{}});let i=p({type:1,follow_status:e,pn:s,ps:r,vmid:n}),l=await fetch(`https://api.bilibili.com/x/space/bangumi/follow/list?${i}`),u=await l.json();return!l.ok||u?.code!==0?c({code:502,message:u.message,data:{}}):c({code:200,message:"ok",data:R(u.data)})}function R(t){return{list:t?.list?.map(e=>{let o=[{label:e?.new_ep?.index_show},{label:"\u8BC4\u5206",value:e?.rating?.score},{label:"\u64AD\u653E\u91CF",value:e?.stat?.view&&m(e.stat.view)},{label:"\u8FFD\u756A\u6570",value:e?.stat?.follow&&m(e.stat.follow)},{label:"\u6295\u5E01\u6570",value:e?.stat?.coin&&m(e.stat.coin)},{label:"\u5F39\u5E55\u6570",value:e?.stat?.danmaku&&m(e.stat.danmaku)}],s=e.cover;if(s&&s.startsWith("http:")){let r=new URL(s);r.protocol="https:",s=r.toString()}return{nameCN:e.title,summary:e.summary,cover:s,url:e.url,labels:o.filter(r=>r.label)}})??[],pageNumber:t.pn,pageSize:t.ps,total:t.total,totalPages:Math.ceil(t.total/t.ps)}}var x={1:"2",2:"4",3:"1"},N={0:null,1:"1",2:"3",3:"2"};async function y(t,a){let{subjectType:e="1",uid:o,collectionType:s="0",pageNumber:r=1,pageSize:n=10}=t,i=o??a?.BGM;if(!i)return c({code:400,message:"uid is required",data:{}});let l=p({subject_type:x[e],type:N[s],limit:n,offset:(Number(r)-1)*Number(n)}),u=await fetch(`https://api.bgm.tv/v0/users/${i}/collections?${l}`,{headers:{"User-Agent":"yixiaojiu/bilibili-bangumi-component (https://github.com/yixiaojiu/bilibili-bangumi-component)"}}),g=await u.json();return u.ok?c({code:200,message:"ok",data:T(g,{pageNumber:Number(r),pageSize:Number(n)})}):c({code:502,message:g.description,data:{}})}function T(t,a){return{list:t?.data?.map(o=>{let s=o?.subject,r=[{label:s?.eps&&`${s.eps}\u8BDD`},{label:"\u8BC4\u5206",value:s?.score},{label:"\u6392\u540D",value:s?.rank},{label:"\u65F6\u95F4",value:s?.date}];return{name:s?.name,nameCN:s?.name_cn,summary:s?.short_summary,cover:s?.images?.large,url:s?.id?`https://bgm.tv/subject/${s?.id}`:"https://bgm.tv/",labels:r.filter(n=>"value"in n?n.value:n.label)}})??[],...a,total:t.total,totalPages:Math.ceil(t.total/a.pageSize)}}function S(t){let{pageNumber:a=1,pageSize:e=10}=t;return{...t,pageNumber:Number(a),pageSize:Number(e)}}var F="edge",G=["hkg1","hnd1","kix1","sin1"];async function H(t){let a=new URL(t.url),e=S(f(a));if(isMock){if(a.pathname.endsWith("bilibili"))return Response.json(mockDataMap.bilibili);if(a.pathname.endsWith("bgm"))return Response.json(mockDataMap.bgm)}let o={};try{o=customData}catch{}return a.pathname.endsWith("bilibili")?await h(e,process.env):a.pathname.endsWith("bgm")?await y(e,process.env):a.pathname.endsWith("custom")?d(e,o):Response.json({code:404,message:"not found",data:{}},{status:404})}export{H as GET,G as preferredRegion,F as runtime}; 3 | -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/index.css: -------------------------------------------------------------------------------- 1 | @layer bilibili-bangumi-component { 2 | :host { 3 | display: block; 4 | /* 基础文本颜色 */ 5 | --bbc-text-base-color: #4c4948; 6 | /* 标签颜色 */ 7 | --bbc-label-color: #FF9843; 8 | /* 下划线、背景之类的颜色 */ 9 | --bbc-primary-color: #425aef; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | } 19 | 20 | .bbc-tabs { 21 | display: flex; 22 | gap: 8px; 23 | } 24 | 25 | .bbc-tab-item { 26 | padding: 4px 16px; 27 | border-radius: 8px; 28 | font-size: 14px; 29 | font-weight: bold; 30 | color: var(--bbc-text-base-color); 31 | cursor: pointer; 32 | } 33 | 34 | .bbc-md-tab-item { 35 | padding: 4px 12px; 36 | } 37 | 38 | .bbc-tab-item:hover { 39 | background-color: var(--bbc-primary-color); 40 | color: white; 41 | } 42 | 43 | .bbc-tab-item-active { 44 | color: white; 45 | background-color: var(--bbc-primary-color); 46 | } 47 | 48 | .bbc-header-platform { 49 | display: flex; 50 | align-items: center; 51 | flex-wrap: wrap; 52 | gap: 16px; 53 | margin-bottom: 16px; 54 | } 55 | 56 | .bbc-header-platform .divider { 57 | width: 3px; 58 | border-radius: 2px; 59 | height: 32px; 60 | background-color: var(--bbc-primary-color); 61 | } 62 | 63 | .bbc-bangumi:last-child { 64 | border-bottom: none; 65 | } 66 | 67 | .bbc-bangumi-item { 68 | display: flex; 69 | margin: 12px 0; 70 | gap: 16px; 71 | padding: 16px; 72 | border-bottom: 1px solid #ddd; 73 | } 74 | 75 | .bbc-bangumi-item img { 76 | border-radius: 8px; 77 | cursor: pointer; 78 | height: 140px; 79 | object-fit: cover; 80 | aspect-ratio: 3 / 4; 81 | } 82 | 83 | .bbc-bangumi-content { 84 | flex: 1; 85 | display: flex; 86 | flex-direction: column; 87 | justify-content: space-between; 88 | } 89 | 90 | .bbc-bangumi-title { 91 | margin: 0; 92 | margin-bottom: 8px; 93 | } 94 | 95 | .bbc-bangumi-title a { 96 | color: var(--bbc-text-base-color); 97 | } 98 | 99 | .bbc-bangumi-title a:hover { 100 | border-bottom: 2px solid var(--bbc-primary-color); 101 | } 102 | 103 | .bbc-bangumi-title small { 104 | margin-left: 8px; 105 | font-weight: normal; 106 | font-size: 14px; 107 | color: var(--bbc-text-base-color); 108 | height: 24px; 109 | } 110 | 111 | .bbc-bangumi-labels { 112 | color: var(--bbc-label-color); 113 | display: flex; 114 | } 115 | 116 | .bbc-bangumi-label { 117 | display: flex; 118 | flex-direction: column; 119 | align-items: center; 120 | justify-content: space-between; 121 | padding: 0 12px; 122 | height: 36px; 123 | font-size: 14px; 124 | border-right: 1px solid var(--bbc-label-color); 125 | } 126 | 127 | .bbc-bangumi-label:last-child { 128 | border-right: none; 129 | } 130 | 131 | .bbc-bangumi-label p { 132 | margin: 0; 133 | padding: 0 4px; 134 | } 135 | 136 | .bbc-bangumi-label:not(.bbc-bangumi-label > .bbc-bangumi-label-title) { 137 | justify-content: center; 138 | } 139 | 140 | .bbc-md-label-text { 141 | font-size: 12px; 142 | } 143 | 144 | .bbc-bangumi-summary { 145 | margin-bottom: 0; 146 | font-size: 12px; 147 | color: var(--bbc-text-base-color); 148 | 149 | overflow: hidden; 150 | text-overflow: ellipsis; 151 | display: -webkit-box; 152 | -webkit-box-orient: vertical; 153 | -webkit-line-clamp: 2; 154 | } 155 | 156 | .bbc-pagination { 157 | height: 40px; 158 | text-align: center; 159 | padding-right: 20px; 160 | } 161 | 162 | .bbc-pagination-button { 163 | border-radius: 4px; 164 | font-size: 14px; 165 | padding: 4px 8px; 166 | margin: 0 4px; 167 | color: #bbb; 168 | cursor: pointer; 169 | } 170 | 171 | .bbc-pagination-button:hover { 172 | background-color: var(--bbc-primary-color); 173 | color: white; 174 | } 175 | 176 | .bbc-pagination-pagenum { 177 | color: #bbb; 178 | font-size: 14px; 179 | } 180 | 181 | .bbc-pagination-input { 182 | display: inline-block; 183 | color: #bbb; 184 | font-size: 12px; 185 | } 186 | 187 | .bbc-pagination-input input { 188 | display: inline-block; 189 | margin: 0 8px; 190 | font-size: 12px; 191 | color: #bbb; 192 | width: 30px; 193 | height: 24px; 194 | line-height: 24px; 195 | border-radius: 4px; 196 | border: 1px solid #bbb; 197 | text-align: center; 198 | outline: none; 199 | transition: border .2s ease; 200 | } 201 | 202 | .bbc-skeleton-container { 203 | animation: skeleton-blink 1.2s ease-in-out infinite; 204 | } 205 | 206 | @keyframes skeleton-blink { 207 | 50% { 208 | opacity: 0.6; 209 | } 210 | } 211 | 212 | .bbc-skeleton-avatar { 213 | height: 140px; 214 | border-radius: 12px; 215 | background-color: #f2f3f5; 216 | aspect-ratio: 3 / 4; 217 | } 218 | 219 | .bbc-skeleton-row { 220 | height: 24px; 221 | border-radius: 8px; 222 | background-color: #f2f3f5; 223 | } 224 | 225 | .bbc-empty { 226 | display: flex; 227 | padding: 40px 0; 228 | justify-content: center; 229 | align-items: center; 230 | } 231 | 232 | .bbc-empty img { 233 | height: 140px; 234 | object-fit: cover; 235 | } 236 | 237 | .bbc-error { 238 | margin: 20px 0; 239 | display: flex; 240 | gap: 12px; 241 | flex-direction: column; 242 | justify-content: center; 243 | align-items: center; 244 | } 245 | 246 | .bbc-error img { 247 | display: block; 248 | width: 260px; 249 | } 250 | 251 | .bbc-error p { 252 | margin: 0; 253 | color: var(--bbc-text-base-color); 254 | font-size: 18px; 255 | } 256 | } -------------------------------------------------------------------------------- /packages/bilibili-bangumi-component/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State, h } from '@stencil/core' 2 | import type { Collection, CollectionLabel, ContainerState, Platform, ResponseData, Subject } from '../shared/types' 3 | import type { UnionToTuple } from '../shared/typeUtils' 4 | import type { BilibiliParams } from '../shared/api' 5 | import { getBangumi, getBilibili, getCustom } from '../shared/api' 6 | import { thorttle } from '../shared/utils' 7 | import { collectionLabelMap } from '../shared/dataMap' 8 | import { Tabs } from './Tabs' 9 | import { List } from './List' 10 | import { type ChangeType, Pagination } from './Pagination' 11 | import { Skeleton } from './Skeleton' 12 | import { Empty } from './Empty' 13 | import { Error } from './Error' 14 | 15 | @Component({ 16 | tag: 'bilibili-bangumi', 17 | styleUrl: 'index.css', 18 | shadow: true, 19 | }) 20 | export class BilibiliBangumi { 21 | @Prop() api: string 22 | @Prop() bilibiliUid?: string 23 | @Prop() bgmUid?: string 24 | @Prop() bilibiliEnabled = true 25 | @Prop() bgmEnabled = true 26 | @Prop() pageSize = 15 27 | @Prop() customEnabled = false 28 | @Prop() customLabel = '自定义' 29 | 30 | @State() loading = false 31 | @State() error?: Error 32 | 33 | @State() pageNumber = 1 34 | @State() responseData: ResponseData 35 | 36 | platformLabels: Platform[] = ['Bilibili', 'Bangumi'] 37 | @State() activePlatform: Platform = 'Bilibili' 38 | 39 | subjectLabels: UnionToTuple = ['动画', '游戏', '书籍'] 40 | @State() activeSubject: Subject = '动画' 41 | 42 | @State() collectionLabels: CollectionLabel = ['全部', '想看', '在看', '看过'] 43 | @State() activeCollection: Collection = '全部' 44 | 45 | @State() containerRef: HTMLDivElement = null 46 | @State() containerState: ContainerState = 'large' 47 | 48 | componentWillLoad() { 49 | const platformLabels = [...this.platformLabels] 50 | if (this.customEnabled) 51 | platformLabels.push(this.customLabel) 52 | const filterArr = [this.bilibiliEnabled, this.bgmEnabled, this.customEnabled] 53 | this.platformLabels = platformLabels.filter((_, index) => filterArr[index]) 54 | this.activePlatform = this.platformLabels[0] 55 | this.fetchData() 56 | } 57 | 58 | componentDidLoad() { 59 | // 监听容器尺寸变化 60 | const update = thorttle((width: number) => { 61 | let containerState: ContainerState = 'large' 62 | if (width <= 640) 63 | containerState = 'middle' 64 | if (width <= 465) 65 | containerState = 'small' 66 | 67 | this.containerState = containerState 68 | }, 100).bind(this) 69 | 70 | const resizeObserver = new ResizeObserver((entries) => { 71 | for (const entry of entries) 72 | update(entry.contentRect.width) 73 | }) 74 | resizeObserver.observe(this.containerRef) 75 | } 76 | 77 | private fetchData = async () => { 78 | try { 79 | this.loading = true 80 | this.error = null 81 | let response 82 | const bilibiliParams: BilibiliParams = { 83 | uid: this.bilibiliUid, 84 | collectionType: this.activeCollection, 85 | pageSize: this.pageSize, 86 | pageNumber: this.pageNumber, 87 | } 88 | if (this.activePlatform === 'Bilibili') { 89 | response = await getBilibili(this.api, bilibiliParams) 90 | } 91 | else if (this.activePlatform === 'Bangumi') { 92 | response = await getBangumi(this.api, { 93 | ...bilibiliParams, 94 | uid: this.bgmUid, 95 | subjectType: this.activeSubject, 96 | }) 97 | } 98 | else { 99 | response = await getCustom(this.api, { 100 | ...bilibiliParams, 101 | subjectType: this.activeSubject, 102 | }) 103 | } 104 | if (response.code === 200) { 105 | this.responseData = response.data 106 | } 107 | else { 108 | this.error = response 109 | this.responseData = null 110 | } 111 | } 112 | catch (error) { 113 | this.error = error 114 | this.responseData = null 115 | } 116 | this.loading = false 117 | } 118 | 119 | private handlePlatformChange = (label: Platform) => { 120 | this.collectionLabels = collectionLabelMap['动画'] 121 | this.activePlatform = label 122 | this.pageNumber = 1 123 | this.activeSubject = '动画' 124 | this.activeCollection = '全部' 125 | this.fetchData() 126 | } 127 | 128 | private handleSubjectChange = (label: Subject) => { 129 | this.collectionLabels = collectionLabelMap[label] 130 | this.activeSubject = label 131 | this.pageNumber = 1 132 | this.activeCollection = '全部' 133 | this.fetchData() 134 | } 135 | 136 | private handleCollectionChange = (label: Collection) => { 137 | this.activeCollection = label 138 | this.pageNumber = 1 139 | this.fetchData() 140 | } 141 | 142 | private scrollToTop = () => { 143 | document.documentElement.scrollTo({ 144 | top: 0, 145 | behavior: 'smooth', 146 | }) 147 | } 148 | 149 | private handlePageChange = (changeType: ChangeType) => { 150 | const { totalPages } = this.responseData! 151 | switch (changeType) { 152 | case 'head': 153 | this.pageNumber = 1 154 | break 155 | case 'prev': 156 | if (this.pageNumber === 1) 157 | return 158 | this.pageNumber-- 159 | break 160 | case 'next': 161 | if (this.pageNumber === totalPages) 162 | return 163 | this.pageNumber++ 164 | break 165 | case 'tail': 166 | this.pageNumber = totalPages 167 | break 168 | } 169 | this.scrollToTop() 170 | this.fetchData() 171 | } 172 | 173 | private handleInputChange = (event: Event) => { 174 | const inputValue = Number.parseInt((event.target as HTMLInputElement).value) 175 | if (Object.is(inputValue, Number.NaN)) 176 | return 177 | const { totalPages } = this.responseData! 178 | if (inputValue < 1) 179 | this.pageNumber = 1 180 | else if (inputValue > totalPages) 181 | this.pageNumber = totalPages 182 | else 183 | this.pageNumber = inputValue 184 | this.scrollToTop() 185 | this.fetchData() 186 | } 187 | 188 | render() { 189 | return ( 190 |
this.containerRef = ele}> 191 |
192 | 193 | { this.activePlatform !== 'Bilibili' &&
} 194 | { 195 | this.activePlatform !== 'Bilibili' 196 | && 197 | } 198 |
199 |
200 | 201 |
202 | {this.loading && !this.responseData && } 203 | {this.error && } 204 | {this.responseData && } 205 | {this.responseData && this.responseData.total === 0 && } 206 | {this.responseData && ( 207 | 213 | )} 214 |
215 | ) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /packages/api/src/mock/bili-bgm.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unused-imports/no-unused-vars */ 2 | const bilibiliMockData = { 3 | code: 200, 4 | message: 'ok', 5 | data: { 6 | list: [ 7 | { 8 | nameCN: '前进吧!登山少女 第二季', 9 | summary: '故事承接第一季剧情,葵在重遇喜欢户外活动的青梅竹马朋友日向后,再次进行登山活动,实现了再一次看到小时候看过的山顶日出这样想法,并且与枫和心夏成为了好朋友。\n而接下来,葵她们将不断的挑战着自己,决定登上富士山。', 10 | cover: 'http://i0.hdslb.com/bfs/bangumi/47c46badc1c450f22f36b5d27304591db9b0a8c1.jpg', 11 | url: 'https://www.bilibili.com/bangumi/play/ss4115', 12 | labels: [ 13 | { 14 | label: '全24话', 15 | }, 16 | { 17 | label: '评分', 18 | value: 9.8, 19 | }, 20 | { 21 | label: '播放量', 22 | value: '399.2万', 23 | }, 24 | { 25 | label: '追番数', 26 | value: '14.4万', 27 | }, 28 | { 29 | label: '投币数', 30 | value: '2.6万', 31 | }, 32 | { 33 | label: '弹幕数', 34 | value: '13.3万', 35 | }, 36 | ], 37 | }, 38 | { 39 | nameCN: '前进吧!登山少女', 40 | summary: '作品以女子高中生登山为主题,讲述了以喜好室内活动并且有恐高症的主人公雪村葵与有着户外活动意向的青梅竹马日向的再会作为契机,两人为了能再次看到年幼时看过的山顶日出而登山的故事。', 41 | cover: 'http://i0.hdslb.com/bfs/bangumi/ffaba15ad1e7d9d749c86995054ca4ff1975326f.jpg', 42 | url: 'https://www.bilibili.com/bangumi/play/ss4145', 43 | labels: [ 44 | { 45 | label: '全12话', 46 | }, 47 | { 48 | label: '评分', 49 | value: 9.7, 50 | }, 51 | { 52 | label: '播放量', 53 | value: '476.2万', 54 | }, 55 | { 56 | label: '追番数', 57 | value: '38.4万', 58 | }, 59 | { 60 | label: '投币数', 61 | value: '1.7万', 62 | }, 63 | { 64 | label: '弹幕数', 65 | value: '2.5万', 66 | }, 67 | ], 68 | }, 69 | { 70 | nameCN: '终将成为你', 71 | summary: '不知“爱”为何物,不懂得与人恋爱心情的小糸侑,在接到要好的男生告白后,因回复他而陷入迷茫。\n“要不要加入学生会?”在犹豫加入哪个社团的小糸侑,在听到了老师的建议后,稍有兴趣的她拿着地图前往了学生会室,在途中她邂逅的学生会成员七海灯子,\n“不论被谁告白,都无法喜欢上对方。”灯子的这句话,让侑感觉找到了与之共鸣的伙伴,侑向灯子诉说了自己一直以来的烦恼。\n在灯子的鼓励下,侑鼓起勇气拒绝了男生。可是,本以为是自己伙伴的灯子,却在这时——\n', 72 | cover: 'http://i0.hdslb.com/bfs/bangumi/9254b0bafd699c1a778c42658497948ba3038a77.png', 73 | url: 'https://www.bilibili.com/bangumi/play/ss25622', 74 | labels: [ 75 | { 76 | label: '全13话', 77 | }, 78 | { 79 | label: '评分', 80 | value: 9.8, 81 | }, 82 | { 83 | label: '播放量', 84 | value: '3321.7万', 85 | }, 86 | { 87 | label: '追番数', 88 | value: '215万', 89 | }, 90 | { 91 | label: '投币数', 92 | value: '38.6万', 93 | }, 94 | { 95 | label: '弹幕数', 96 | value: '67.6万', 97 | }, 98 | ], 99 | }, 100 | { 101 | nameCN: '一起一起这里那里', 102 | summary: '摘希虽然喜欢伊御,却没办法坦率的表现出来。伊御虽然不清楚摘希的心意,却还是温柔的对待她。两人的关系维持在朋友程度,尚未成为恋人。两人的朋友们总是在一旁热热闹闹的守护他们。\n自2006年10月号以三个月的短篇的形式开始在“まんがタイムきらら”连载。标题的英文为“PLACE TO PLACE”。摘希(尖端出版社译名)和伊御两人微妙的距离感让旁观的人反而比他们更害羞,这时两人的朋友会代替读者对他们吐槽。在第一集书腰上写着“看了之后一定会想要在地上打滚”。', 103 | cover: 'http://i0.hdslb.com/bfs/bangumi/6de861925c772076a747844427bf49c252a8accf.jpg', 104 | url: 'https://www.bilibili.com/bangumi/play/ss782', 105 | labels: [ 106 | { 107 | label: '全13话', 108 | }, 109 | { 110 | label: '评分', 111 | value: 9.8, 112 | }, 113 | { 114 | label: '播放量', 115 | value: '2432.8万', 116 | }, 117 | { 118 | label: '追番数', 119 | value: '170.6万', 120 | }, 121 | { 122 | label: '投币数', 123 | value: '13.5万', 124 | }, 125 | { 126 | label: '弹幕数', 127 | value: '40.9万', 128 | }, 129 | ], 130 | }, 131 | { 132 | nameCN: '文学少女 剧场版', 133 | summary: '故事的主人公高中二年级的平凡男生井上心叶,因为过\n去的阴影,对人抱着冷谈的态度。然而,因为某个契机,井上心叶知道了文学少女天野远子的秘密。得知秘密的他被远子以文艺部部长的身份强制要求加入社团,并且每天被逼着写三题故事。心叶被天真浪漫的远子搞的团团转,虽然对她“这是事件哦”的台词相当头疼,心叶依旧每次帮助她解决事件。在这过程中,两人结识了竹田千爱、樱井流人、姫仓麻贵、琴吹七濑等人。心叶的心房也渐渐打开,就要走出过去的阴影时,却遇到了某个少女。然后当心叶的故事迷题解开,从过去中解放出来之时,心叶却背叛了远子,而远子也背叛了他。千爱、流人、一诗、七濑、麻贵,还有心叶……。当他们的故事因为温柔善良的文学少女告一段落时,属于文学少女的故事开始撰写,而伴随着文学少女的故事,心叶等人的运命,再次开始轮转……', 134 | cover: 'http://i0.hdslb.com/bfs/bangumi/d56a140c087994505e2eec9aa004d163e5128054.jpg', 135 | url: 'https://www.bilibili.com/bangumi/play/ss3575?theme=movie', 136 | labels: [ 137 | { 138 | label: '全1话', 139 | }, 140 | { 141 | label: '评分', 142 | value: 9.4, 143 | }, 144 | { 145 | label: '播放量', 146 | value: '43.2万', 147 | }, 148 | { 149 | label: '追番数', 150 | value: '5.4万', 151 | }, 152 | { 153 | label: '投币数', 154 | value: '3850', 155 | }, 156 | { 157 | label: '弹幕数', 158 | value: '5086', 159 | }, 160 | ], 161 | }, 162 | { 163 | nameCN: '文学少女 今日的点心~初恋~', 164 | summary: '文学少女系列(”文学少女”シリーズ)是小说家野村美月与插画家竹冈美穗所推出的轻小说。获得第一届Light Novel Award的Mystery部门大奖。台湾中文版由尖端出版社发行。文学少女轻小说主系列已完结。2008年开始,文学少女在Gangan Power八月号开始连载改编漫画,由高坂りと负责作画。此外,预定在2010年春天将开播由Production I.G所制作的文学少女剧场版。另指爱好文学的女孩', 165 | cover: 'http://i0.hdslb.com/bfs/bangumi/22bc2d1f6e9eb8d5a9230257f44b61a404a38708.jpg', 166 | url: 'https://www.bilibili.com/bangumi/play/ss3769', 167 | labels: [ 168 | { 169 | label: '全1话', 170 | }, 171 | { 172 | label: '评分', 173 | value: 9.1, 174 | }, 175 | { 176 | label: '播放量', 177 | value: '104.5万', 178 | }, 179 | { 180 | label: '追番数', 181 | value: '5.2万', 182 | }, 183 | { 184 | label: '投币数', 185 | value: '3737', 186 | }, 187 | { 188 | label: '弹幕数', 189 | value: '1.6万', 190 | }, 191 | ], 192 | }, 193 | { 194 | nameCN: '恋爱随意链接', 195 | summary: ' 故事发生在私立山星高中,这所学校的文化研究部内,八重樫太一、永濑伊织、稻叶姬子、桐山唯、青木义文这五名成员面临着一个不可思议的现象。起初是唯和义文两个人在毫无前兆的情况下发生了人格交换,之后周围的其他成员也是如此?!爱与青春的五角悲喜剧就此拉开了帷幕……', 196 | cover: 'http://i0.hdslb.com/bfs/bangumi/8274f1107032a6fe0843dc9cf875d887d6ad0f03.jpg', 197 | url: 'https://www.bilibili.com/bangumi/play/ss713', 198 | labels: [ 199 | { 200 | label: '全17话', 201 | }, 202 | { 203 | label: '评分', 204 | value: 9.5, 205 | }, 206 | { 207 | label: '播放量', 208 | value: '1575.5万', 209 | }, 210 | { 211 | label: '追番数', 212 | value: '134.7万', 213 | }, 214 | { 215 | label: '投币数', 216 | value: '11.4万', 217 | }, 218 | { 219 | label: '弹幕数', 220 | value: '66.6万', 221 | }, 222 | ], 223 | }, 224 | { 225 | nameCN: '轻音少女 第一季', 226 | summary: '【本片翻译由华盟字幕社提供】新学年开始,高中一年级新生平泽唯在误将“轻音乐”当做了“轻便、简易的音乐”,而由于自己小时候玩响板得到老师表扬,于是萌发申请入部的想法。\n另一方面,樱丘高中“轻音部”因原来的部员全部毕业离校,此时轻音部新成员只有秋山澪和田井中律两人,无法满足部员至少四人的最低人数要求即将废部,这下该如何是好呢?\n此外,温柔可爱的千金小姐琴吹䌷被律强拉进入轻音部。\n于是,这四名高一女生机缘巧合聚在了一起,便有了吉他手平泽唯、贝司手秋山澪、鼓手田井中律以及键盘手琴吹䌷,轻音部的故事也由此展开。', 227 | cover: 'http://i0.hdslb.com/bfs/bangumi/8aa0bfce050c72c6626b63d3093a88527c251026.jpg', 228 | url: 'https://www.bilibili.com/bangumi/play/ss1172', 229 | labels: [ 230 | { 231 | label: '全14话', 232 | }, 233 | { 234 | label: '评分', 235 | value: 9.9, 236 | }, 237 | { 238 | label: '播放量', 239 | value: '7592万', 240 | }, 241 | { 242 | label: '追番数', 243 | value: '328万', 244 | }, 245 | { 246 | label: '投币数', 247 | value: '64.7万', 248 | }, 249 | { 250 | label: '弹幕数', 251 | value: '196.9万', 252 | }, 253 | ], 254 | }, 255 | { 256 | nameCN: '银河铁道之夜 2006', 257 | summary: '《银河铁道之夜》说的是一个名叫乔邦尼的男孩,有一天在山丘上睡着了。在睡梦中,他搭上了一趟开往天国的银河铁道列车,和班上他最喜欢的男孩康贝瑞拉一起来到了天国。然而当他醒来,发现这不过是一个梦。但当他跑下山丘回家时,却听到了一个几乎让他不敢相信的消息:康贝瑞拉在河里淹死了。', 258 | cover: 'http://i0.hdslb.com/bfs/bangumi/1a8c32f926ecf9602981e4d0e9a64c01e86ee751.jpg', 259 | url: 'https://www.bilibili.com/bangumi/play/ss3053?theme=movie', 260 | labels: [ 261 | { 262 | label: '全1话', 263 | }, 264 | { 265 | label: '评分', 266 | value: 9.5, 267 | }, 268 | { 269 | label: '播放量', 270 | value: '37.5万', 271 | }, 272 | { 273 | label: '追番数', 274 | value: '4.7万', 275 | }, 276 | { 277 | label: '投币数', 278 | value: '2566', 279 | }, 280 | { 281 | label: '弹幕数', 282 | value: '2483', 283 | }, 284 | ], 285 | }, 286 | { 287 | nameCN: '孤独摇滚!', 288 | summary: '绰号“小孤独”的后藤独,是一个喜爱吉他的孤独少女。经常在家里独自弹奏吉他,但因为一些事情,加入了伊地知虹夏领衔的“纽带乐队”。不敢在他人面前演奏的后藤,能否成为一个出色的乐队成员呢……', 289 | cover: 'https://i0.hdslb.com/bfs/bangumi/image/d9d6284e0919ecfda41981c1f9119f993db62935.jpg', 290 | url: 'https://www.bilibili.com/bangumi/play/ss43164', 291 | labels: [ 292 | { 293 | label: '全12话', 294 | }, 295 | { 296 | label: '评分', 297 | value: 9.9, 298 | }, 299 | { 300 | label: '播放量', 301 | value: '1.4亿', 302 | }, 303 | { 304 | label: '追番数', 305 | value: '358.1万', 306 | }, 307 | { 308 | label: '投币数', 309 | value: '138.6万', 310 | }, 311 | { 312 | label: '弹幕数', 313 | value: '73.5万', 314 | }, 315 | ], 316 | }, 317 | ], 318 | pageNumber: 1, 319 | pageSize: 10, 320 | total: 120, 321 | totalPages: 12, 322 | }, 323 | } 324 | 325 | const bgmMockData = { 326 | code: 200, 327 | message: 'ok', 328 | data: { 329 | list: [ 330 | { 331 | name: 'リズと青い鳥', 332 | nameCN: '莉兹与青鸟', 333 | summary: '铠塚霙,高中3年级,双簧管演奏者。\r\n伞木希美,高中3年级,长笛演奏者。\r\n\r\n初中时,是希美牵起了霙的手,带领她走出了孤独。\r\n自那以后,希美就占据了霙的整个世界。\r\n只有与希美在一起的生活,才令霙感到幸福。\r\n可在高中一年级,希美一度退', 334 | cover: 'https://lain.bgm.tv/pic/cover/l/1d/35/216371_5926R.jpg', 335 | url: 'https://bgm.tv/subject/216371', 336 | labels: [ 337 | { 338 | label: '1话', 339 | }, 340 | { 341 | label: '评分', 342 | value: 8.6, 343 | }, 344 | { 345 | label: '排名', 346 | value: 27, 347 | }, 348 | { 349 | label: '时间', 350 | value: '2018-04-21', 351 | }, 352 | ], 353 | }, 354 | { 355 | name: 'SHIROBAKO', 356 | nameCN: '白箱', 357 | summary: '本作的故事主要围绕追逐梦想的五名女生——宫森葵、安原绘麻、坂木静香、藤堂美沙、今井绿展开,是一部描写以白箱的完成为目标而奋斗的她们,每天遇到的麻烦以及策划工作时碰到的纠葛与挫折,还有制作组在制作作品时的团结和冲突的动画业界的日常的群像剧作品', 358 | cover: 'https://lain.bgm.tv/pic/cover/l/73/26/110467_Fx9tT.jpg', 359 | url: 'https://bgm.tv/subject/110467', 360 | labels: [ 361 | { 362 | label: '24话', 363 | }, 364 | { 365 | label: '评分', 366 | value: 8.7, 367 | }, 368 | { 369 | label: '排名', 370 | value: 17, 371 | }, 372 | { 373 | label: '时间', 374 | value: '2014-10-09', 375 | }, 376 | ], 377 | }, 378 | { 379 | name: '新世紀エヴァンゲリオン', 380 | nameCN: '新世纪福音战士', 381 | summary: '  2000年,一个科学探险队在南极洲针对被称作“第一使徒”亚当的“光之巨人”进行探险。在对其进行接触实验时,“光之巨人”自毁,从而发生了“第二次冲击”,进而导致世界大战。最后,人类人口减半,地轴偏转、气候改变。根据对“第二次冲击”的调查,', 382 | cover: 'https://lain.bgm.tv/pic/cover/l/e5/69/265_Z5Uou.jpg', 383 | url: 'https://bgm.tv/subject/265', 384 | labels: [ 385 | { 386 | label: '26话', 387 | }, 388 | { 389 | label: '评分', 390 | value: 8.8, 391 | }, 392 | { 393 | label: '排名', 394 | value: 9, 395 | }, 396 | { 397 | label: '时间', 398 | value: '1995-10-04', 399 | }, 400 | ], 401 | }, 402 | { 403 | name: '新世紀エヴァンゲリオン劇場版 Air/まごころを、君に', 404 | nameCN: '新世纪福音战士剧场版 Air/真心为你', 405 | summary: '  神秘的EVA零号机驾驶员绫波丽,在与碇元度约定之时,来到了NERV底层中央教条的红色水池边。她的命运似乎就将在此结束。\r\n  在危急时刻,葛城美里不但要完成加持良治交待的对“第二次浩劫”真相以及“人类补完计划”的探查工作,还要保护碇真嗣', 406 | cover: 'https://lain.bgm.tv/pic/cover/l/fe/45/6049_zy52O.jpg', 407 | url: 'https://bgm.tv/subject/6049', 408 | labels: [ 409 | { 410 | label: '2话', 411 | }, 412 | { 413 | label: '评分', 414 | value: 8.9, 415 | }, 416 | { 417 | label: '排名', 418 | value: 6, 419 | }, 420 | { 421 | label: '时间', 422 | value: '1997-07-19', 423 | }, 424 | ], 425 | }, 426 | { 427 | name: 'CLANNAD 〜AFTER STORY〜', 428 | nameCN: '', 429 | summary: '在某个小镇,主角冈崎朋也因为家庭的因素成为不良少年,一直与春原阳平为伍,在光坂高校过着潦倒的生活,但冀望终有一天能够离开所在的小镇。某一天,他在学校坡道前发现了一个止步不前的女孩,在朋也认识了这个名为“古河渚”的女孩后,他的生活开始有了重大', 430 | cover: 'https://lain.bgm.tv/pic/cover/l/67/d1/876_dCfrd.jpg', 431 | url: 'https://bgm.tv/subject/876', 432 | labels: [ 433 | { 434 | label: '24话', 435 | }, 436 | { 437 | label: '评分', 438 | value: 9.1, 439 | }, 440 | { 441 | label: '排名', 442 | value: 3, 443 | }, 444 | { 445 | label: '时间', 446 | value: '2008-10-02', 447 | }, 448 | ], 449 | }, 450 | { 451 | name: 'BanG Dream! It\'s MyGO!!!!!', 452 | nameCN: '', 453 | summary: '「能一辈子和我搞乐队吗?」\r\n\r\n在高一的春季即将结束之时,羽丘女子学园里人人都在参加乐队活动。\r\n入学较晚的爱音,也为了尽快融入班级,急忙寻找乐队成员。\r\n在寻找时,她发现被称为「羽丘的不可思议少女」的灯还没有参加乐队,\r\n于是爱音不由得', 454 | cover: 'https://lain.bgm.tv/pic/cover/l/e7/a7/428735_1v11n.jpg', 455 | url: 'https://bgm.tv/subject/428735', 456 | labels: [ 457 | { 458 | label: '13话', 459 | }, 460 | { 461 | label: '评分', 462 | value: 8.3, 463 | }, 464 | { 465 | label: '排名', 466 | value: 74, 467 | }, 468 | { 469 | label: '时间', 470 | value: '2023-06-29', 471 | }, 472 | ], 473 | }, 474 | ], 475 | pageNumber: 1, 476 | pageSize: 10, 477 | total: 6, 478 | totalPages: 1, 479 | }, 480 | } 481 | 482 | const mockDataMap = { 483 | bilibili: bilibiliMockData, 484 | bgm: bgmMockData, 485 | } 486 | --------------------------------------------------------------------------------