├── .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 |

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 |

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 |
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 |
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 |
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 |
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 | 
11 |
12 | 3. 将 [api/val-town.js](api/val-town.js) 中的代码复制到此处
13 |
14 | 
15 |
16 | 4. (可选)添加 `uid` env
17 |
18 | 
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 | 
31 |
32 | 3. 检查构建记录中是否注册了 Functions
33 |
34 | 
35 |
36 | 
37 |
38 | 
39 |
40 | 4. 如果构建记录中没有 Functions,则是部署资源的根目录出了问题 (如果有 Functions,直接跳过这一步)
41 |
42 | 
43 |
44 | 
45 |
46 | 
47 |
48 | 
49 |
50 | 
51 |
52 | 5. 验证是否部署成功
53 |
54 | 
55 |
56 | 
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 |
20 |
21 |
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 |
47 | {loading
48 | ?
49 | : (
50 |
64 | )}
65 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------