Skip to content

Commit a7b2e31

Browse files
committed
增加b站推荐组件
1 parent 7e3826f commit a7b2e31

File tree

7 files changed

+302
-2
lines changed

7 files changed

+302
-2
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@widget-js/hotspot",
33
"type": "module",
4-
"version": "1.3.0",
4+
"version": "1.4.0",
55
"private": true,
66
"author": "Widget JS",
77
"license": "MIT",
@@ -82,4 +82,4 @@
8282
"eslint --cache --fix"
8383
]
8484
}
85-
}
85+
}
185 KB
Loading

src/api/BilibiliApi.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import axios from 'axios'
2+
3+
// 视频数据接口
4+
interface BilibiliVideoItem {
5+
id: number
6+
bvid: string
7+
cid: number
8+
goto: string
9+
uri: string
10+
pic: string
11+
title: string
12+
duration: number
13+
pubdate: number
14+
owner: {
15+
mid: number
16+
name: string
17+
face: string
18+
}
19+
stat: {
20+
view: number
21+
like: number
22+
danmaku: number
23+
vt: number
24+
}
25+
}
26+
27+
// API响应接口
28+
interface BilibiliApiResponse {
29+
code: number
30+
message: string
31+
ttl: number
32+
data: {
33+
item: BilibiliVideoItem[]
34+
}
35+
}
36+
37+
export class BilibiliApi {
38+
private static readonly BASE_URL = 'https://api.bilibili.com'
39+
40+
/**
41+
* 获取推荐视频列表
42+
* @returns Promise<BilibiliVideoItem[]>
43+
*/
44+
public static async getRecommendedVideos(lasstShowList: string[] = []): Promise<BilibiliVideoItem[]> {
45+
try {
46+
const path = 'brush=4&homepage_ver=1&ps=10&last_y_num=5'
47+
const response = await axios.get<BilibiliApiResponse>(
48+
`${this.BASE_URL}/x/web-interface/wbi/index/top/feed/rcmd?${path}`,
49+
{
50+
params: {
51+
last_show_list: lasstShowList.join(','),
52+
w_ts: Math.floor(Date.now() / 1000),
53+
fresh_idx: Math.floor(Date.now() / 1000),
54+
},
55+
},
56+
)
57+
58+
if (response.data.code === 0) {
59+
return response.data.data.item
60+
}
61+
else {
62+
throw new Error(`Bilibili API Error: ${response.data.message}`)
63+
}
64+
}
65+
catch (error) {
66+
console.error('Failed to fetch Bilibili recommended videos:', error)
67+
throw error
68+
}
69+
}
70+
}
71+
72+
export type { BilibiliVideoItem }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Widget, WidgetKeyword } from '@widget-js/core'
2+
3+
const BilibiliRcmdWidget = new Widget({
4+
name: 'cn.widgetjs.widgets.hotspot.bilibili_rcmd',
5+
previewImage: '/images/preview_bilibili_rcmd.png',
6+
title: { 'zh-CN': 'B站推荐', 'en-US': 'Bilibili Recommend' },
7+
description: { 'zh-CN': '实时获取B站推荐视频,发现更多精彩内容', 'en-US': 'Discover more exciting content' },
8+
keywords: [WidgetKeyword.RECOMMEND],
9+
categories: ['news'],
10+
lang: 'zh-CN',
11+
width: 4,
12+
height: 4,
13+
minWidth: 4,
14+
maxWidth: 6,
15+
minHeight: 4,
16+
maxHeight: 6,
17+
path: '/widget/bilibili_rcmd',
18+
socialLinks: [
19+
{ name: 'github', link: 'https://github.com/widget-js/hotspot' },
20+
],
21+
})
22+
23+
export default BilibiliRcmdWidget
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { RouteRecordRaw } from 'vue-router'
2+
import BilibiliRcmdWidget from './BilibiliRcmd.widget'
3+
4+
const path = BilibiliRcmdWidget.path
5+
const name = BilibiliRcmdWidget.name
6+
7+
const BilibiliRcmdWidgetRoutes: RouteRecordRaw[] = [
8+
{
9+
path,
10+
name: `${name}`,
11+
component: () =>
12+
import(
13+
/* webpackChunkName: "cn.widgetjs.widgets.hotspot.bilibili_rcmd" */ './BilibiliRcmdWidgetView.vue'
14+
),
15+
},
16+
]
17+
18+
export default BilibiliRcmdWidgetRoutes
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<script lang="ts" setup>
2+
import { useWidget } from '@widget-js/vue3'
3+
import { onMounted, ref } from 'vue'
4+
import { ElScrollbar } from 'element-plus'
5+
import { useIntervalFn, useWindowSize } from '@vueuse/core'
6+
import { BrowserWindowApi } from '@widget-js/core'
7+
import type { BilibiliVideoItem } from '@/api/BilibiliApi'
8+
import { BilibiliApi } from '@/api/BilibiliApi'
9+
10+
useWidget()
11+
12+
const recommendedVideos = ref<BilibiliVideoItem[]>([])
13+
14+
async function refresh() {
15+
const latestShowVid = recommendedVideos.value.map(it => `av_${it.id}`)
16+
recommendedVideos.value = await BilibiliApi.getRecommendedVideos(latestShowVid)
17+
}
18+
const { height } = useWindowSize()
19+
20+
onMounted(async () => {
21+
refresh()
22+
})
23+
24+
function openLink(url: string) {
25+
BrowserWindowApi.openUrl(url, { external: true })
26+
}
27+
28+
useIntervalFn(() => {
29+
refresh()
30+
}, 20 * 60 * 1000)
31+
</script>
32+
33+
<template>
34+
<widget-wrapper class="bilibili-rcmd">
35+
<div class="root">
36+
<div class="header">
37+
<img src="@/widgets/bilibili/images/bilibili_pink.png" alt="bilibili" class="logo">
38+
<div class="search-bar">
39+
<div class="input" @click="openLink('https://search.bilibili.com/all?search_source=1')">
40+
搜索你感兴趣的视频
41+
</div>
42+
<div class="refresh-icon" @click="refresh">
43+
44+
</div>
45+
</div>
46+
</div>
47+
<ElScrollbar :height="height - 70">
48+
<div class="video-list">
49+
<div v-for="video in recommendedVideos" :key="video.bvid" class="video-item" @click="openLink(video.uri)">
50+
<div class="thumbnail">
51+
<img :src="video.pic" :alt="video.title">
52+
<div class="view-count">
53+
{{ (video.stat.view / 10000).toFixed(1) }}万
54+
</div>
55+
</div>
56+
<div class="video-info">
57+
<div class="title">
58+
{{ video.title }}
59+
</div>
60+
<div class="uploader">
61+
<span class="up-icon">UP</span>
62+
{{ video.owner.name }}
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
</ElScrollbar>
68+
</div>
69+
</widget-wrapper>
70+
</template>
71+
72+
<style lang="scss" scoped>
73+
.root {
74+
color: var(--widget-color);
75+
}
76+
77+
img {
78+
-webkit-user-drag: none;
79+
}
80+
81+
.video-list {
82+
display: grid;
83+
padding: 12px 12px 0 12px;
84+
grid-template-columns: repeat(2, 1fr);
85+
grid-gap: 10px;
86+
gap: 16px;
87+
88+
@media (min-width: 500px) {
89+
grid-template-columns: repeat(3, 1fr);
90+
}
91+
}
92+
93+
.header {
94+
padding: 12px 12px 0 12px;
95+
display: flex;
96+
align-items: center;
97+
98+
.logo {
99+
height: 18px;
100+
margin-right: 12px;
101+
}
102+
103+
.search-bar {
104+
flex: 1;
105+
display: flex;
106+
cursor: pointer;
107+
align-items: center;
108+
background: color-mix(in srgb, var(--widget-background-color), var(--widget-color) 20%);
109+
border-radius: 20px;
110+
padding: 4px 16px;
111+
112+
.input {
113+
flex: 1;
114+
border: none;
115+
background: transparent;
116+
outline: none;
117+
font-size: 12px;
118+
}
119+
120+
.refresh-icon {
121+
cursor: pointer;
122+
color: #999;
123+
}
124+
}
125+
}
126+
127+
.video-item {
128+
cursor: pointer;
129+
130+
.thumbnail {
131+
position: relative;
132+
aspect-ratio: 16/10;
133+
border-radius: 8px;
134+
overflow: hidden;
135+
136+
img {
137+
width: 100%;
138+
height: 100%;
139+
object-fit: cover;
140+
}
141+
142+
.view-count {
143+
position: absolute;
144+
bottom: 8px;
145+
right: 8px;
146+
background: rgba(0, 0, 0, 0.6);
147+
color: white;
148+
padding: 2px 6px;
149+
border-radius: 4px;
150+
font-size: 12px;
151+
}
152+
}
153+
154+
.video-info {
155+
padding: 8px 0;
156+
157+
.title {
158+
font-size: 14px;
159+
font-weight: bold;
160+
margin-bottom: 4px;
161+
overflow: hidden;
162+
text-overflow: ellipsis;
163+
display: -webkit-box;
164+
-webkit-line-clamp: 2;
165+
-webkit-box-orient: vertical;
166+
}
167+
168+
.uploader {
169+
font-size: 12px;
170+
opacity: 0.8;
171+
display: flex;
172+
align-items: center;
173+
174+
.up-icon {
175+
background: #FF6699;
176+
color: white;
177+
padding: 1px 4px;
178+
border-radius: 2px;
179+
margin-right: 4px;
180+
font-size: 10px;
181+
}
182+
}
183+
}
184+
}
185+
</style>

src/widgets/widget-router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import BilibiliWidgetRoutes from './bilibili/BilibiliWidgetRoutes'
22
import WeiBoWidgetRoutes from './weibo/WeiBoWidgetRoutes'
33
import DouyinWidgetRoutes from './douyin/DouyinWidgetRoutes'
44
import BangumiWidgetRoutes from './bangumi/BangumiWidgetRoutes'
5+
import BilibiliRcmdWidgetRoutes from './bilibili-rcmd/BilibiliRcmdWidgetRoutes'
56
import ZhihuWidgetRoutes from '@/widgets/zhihu/ZhihuWidgetRoutes'
67

78
// FBI WANING! IMPORT PLACE, DONT DELETE THIS LINE
@@ -12,6 +13,7 @@ const WidgetRouter = [
1213
...WeiBoWidgetRoutes,
1314
...DouyinWidgetRoutes,
1415
...BangumiWidgetRoutes,
16+
...BilibiliRcmdWidgetRoutes,
1517
// FBI WANING! ROUTE PLACE, DONT DELETE THIS LINE
1618
]
1719
export default WidgetRouter

0 commit comments

Comments
 (0)