mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge tag 'v2.10.2-preview' into preview-fixes
This commit is contained in:
commit
afa034db9a
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -99,6 +99,7 @@
|
|||||||
"superchat",
|
"superchat",
|
||||||
"Swiper",
|
"Swiper",
|
||||||
"tabindex",
|
"tabindex",
|
||||||
|
"tagid",
|
||||||
"Tampermonkey",
|
"Tampermonkey",
|
||||||
"Touhou",
|
"Touhou",
|
||||||
"truetype",
|
"truetype",
|
||||||
|
|||||||
@ -111,6 +111,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/components/live/badge-helper.js",
|
"fullRelativePath": "../../registry/dist/components/live/badge-helper.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/live/badge-helper.js"
|
"fullAbsolutePath": "registry/dist/components/live/badge-helper.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "component",
|
||||||
|
"name": "badgeKeepalive",
|
||||||
|
"displayName": "一键点亮直播间粉丝勋章",
|
||||||
|
"description": "by [@magicFeirl](https://github.com/magicFeirl)\n\n在直播间页面的功能面板添加一键点亮粉丝勋章功能,仅适用于有粉丝勋章且正在直播的直播间。原理:发送一个 300 次点赞的请求点亮粉丝勋章。",
|
||||||
|
"fullRelativePath": "../../registry/dist/components/live/badge-keepalive.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/components/live/badge-keepalive.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "liveChatPanelFit",
|
"name": "liveChatPanelFit",
|
||||||
@ -503,6 +511,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/components/touch/player-gestures.js",
|
"fullRelativePath": "../../registry/dist/components/touch/player-gestures.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/touch/player-gestures.js"
|
"fullAbsolutePath": "registry/dist/components/touch/player-gestures.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "component",
|
||||||
|
"name": "activeVideoLinks",
|
||||||
|
"displayName": "视频链接增强",
|
||||||
|
"description": "by [@Alan Ye](https://github.com/at-wr)\n\n这个组件会将视频简介中的普通网址转换为可点击的链接,并将<del>被 Bilibili 抛弃</del>已失效的 `acg.tv` 跳转链接修复为 `nicovideo.jp` 链接。\r\n\r\n例如: `https://acg.tv/sm37507315` → `https://www.nicovideo.jp/watch/sm37507315`\r\n\r\n<i>本组件不会保证目标链接的安全性,因此在点击前请自行验证其是否可信</i>\r\n",
|
||||||
|
"fullRelativePath": "../../registry/dist/components/utils/active-video-links.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/components/utils/active-video-links.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "albumPubTimeShow",
|
"name": "albumPubTimeShow",
|
||||||
@ -591,6 +607,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/components/utils/download-audio.js",
|
"fullRelativePath": "../../registry/dist/components/utils/download-audio.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/utils/download-audio.js"
|
"fullAbsolutePath": "registry/dist/components/utils/download-audio.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "component",
|
||||||
|
"name": "downloadEmoticons",
|
||||||
|
"displayName": "下载表情",
|
||||||
|
"description": "by [@Pencilqaq](https://github.com/pencilqaq)\n\n## 下载up主专属表情包\r\n\r\n支持下载up主专属表情包,处于任意直播间页面时,下载按钮会在`功能`面板显示,以压缩包形式保存。",
|
||||||
|
"fullRelativePath": "../../registry/dist/components/utils/download-emoticons.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/components/utils/download-emoticons.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "imageExporter",
|
"name": "imageExporter",
|
||||||
|
|||||||
@ -136,6 +136,17 @@
|
|||||||
|
|
||||||
在直播区中, 可从功能面板中直接切换勋章和头衔. 默认显示 256 个 (同时也是上限), 可在选项中修改.
|
在直播区中, 可从功能面板中直接切换勋章和头衔. 默认显示 256 个 (同时也是上限), 可在选项中修改.
|
||||||
|
|
||||||
|
### [一键点亮直播间粉丝勋章](../../registry/dist/components/live/badge-keepalive.js)
|
||||||
|
`badgeKeepalive`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/live/badge-keepalive.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/live/badge-keepalive.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/live/badge-keepalive.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/live/badge-keepalive.js)
|
||||||
|
|
||||||
|
by [@magicFeirl](https://github.com/magicFeirl)
|
||||||
|
|
||||||
|
在直播间页面的功能面板添加一键点亮粉丝勋章功能,仅适用于有粉丝勋章且正在直播的直播间。原理:发送一个 300 次点赞的请求点亮粉丝勋章。
|
||||||
|
|
||||||
### [直播间网页全屏自适应](../../registry/dist/components/live/chat-panel-fit.js)
|
### [直播间网页全屏自适应](../../registry/dist/components/live/chat-panel-fit.js)
|
||||||
`liveChatPanelFit`
|
`liveChatPanelFit`
|
||||||
|
|
||||||
@ -657,6 +668,21 @@ by [@RieN7](https://github.com/rien7),[@ZiuChen](https://github.com/ZiuChen),[@L
|
|||||||
- 进度调整可在左上角和右上角取消
|
- 进度调整可在左上角和右上角取消
|
||||||
- 进度调整时在不同位置滑动, 可以使用3档不同的灵敏度.
|
- 进度调整时在不同位置滑动, 可以使用3档不同的灵敏度.
|
||||||
|
|
||||||
|
### [视频链接增强](../../registry/dist/components/utils/active-video-links.js)
|
||||||
|
`activeVideoLinks`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/active-video-links.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/active-video-links.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/active-video-links.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/active-video-links.js)
|
||||||
|
|
||||||
|
by [@Alan Ye](https://github.com/at-wr)
|
||||||
|
|
||||||
|
这个组件会将视频简介中的普通网址转换为可点击的链接,并将<del>被 Bilibili 抛弃</del>已失效的 `acg.tv` 跳转链接修复为 `nicovideo.jp` 链接。
|
||||||
|
|
||||||
|
例如: `https://acg.tv/sm37507315` → `https://www.nicovideo.jp/watch/sm37507315`
|
||||||
|
|
||||||
|
<i>本组件不会保证目标链接的安全性,因此在点击前请自行验证其是否可信</i>
|
||||||
|
|
||||||
### [相簿发布时间显示](../../registry/dist/components/utils/album-time-show.js)
|
### [相簿发布时间显示](../../registry/dist/components/utils/album-time-show.js)
|
||||||
`albumPubTimeShow`
|
`albumPubTimeShow`
|
||||||
|
|
||||||
@ -783,6 +809,19 @@ by [@snowraincloud](https://github.com/snowraincloud)
|
|||||||
|
|
||||||
> 需要进入音频的详细信息页面才能下载, 在其他页面中此按钮将不可点击.
|
> 需要进入音频的详细信息页面才能下载, 在其他页面中此按钮将不可点击.
|
||||||
|
|
||||||
|
### [下载表情](../../registry/dist/components/utils/download-emoticons.js)
|
||||||
|
`downloadEmoticons`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/download-emoticons.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/download-emoticons.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/download-emoticons.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/download-emoticons.js)
|
||||||
|
|
||||||
|
by [@Pencilqaq](https://github.com/pencilqaq)
|
||||||
|
|
||||||
|
## 下载up主专属表情包
|
||||||
|
|
||||||
|
支持下载up主专属表情包,处于任意直播间页面时,下载按钮会在`功能`面板显示,以压缩包形式保存。
|
||||||
|
|
||||||
### [图片批量导出](../../registry/dist/components/utils/image-exporter.js)
|
### [图片批量导出](../../registry/dist/components/utils/image-exporter.js)
|
||||||
`imageExporter`
|
`imageExporter`
|
||||||
|
|
||||||
|
|||||||
57
registry/lib/components/feeds/extend-live/ExtraOptions.vue
Normal file
57
registry/lib/components/feeds/extend-live/ExtraOptions.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="extend-feeds-live-extra-options">
|
||||||
|
<div class="extend-feeds-live-extra-option">
|
||||||
|
<div class="extend-feeds-live-extra-option-header">置顶列表</div>
|
||||||
|
<div class="extend-feeds-live-extra-option-description">
|
||||||
|
配置直播信息扩充要展示的置顶列表,置顶列表中的用户会排序到上方。
|
||||||
|
</div>
|
||||||
|
<FollowingListSelect :value="pinnedListID" @change="handlePinnedListChange" />
|
||||||
|
</div>
|
||||||
|
<div class="extend-feeds-live-extra-option">
|
||||||
|
<div class="extend-feeds-live-extra-option-header">隐藏列表</div>
|
||||||
|
<div class="extend-feeds-live-extra-option-description">
|
||||||
|
配置直播信息扩充要展示的隐藏列表,隐藏列表中的用户会被隐藏,优先级高于置顶列表。
|
||||||
|
</div>
|
||||||
|
<FollowingListSelect :value="hiddenListID" @change="handleHiddenListChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import FollowingListSelect from './FollowingListSelect.vue'
|
||||||
|
import { getComponentSettings } from '@/core/settings'
|
||||||
|
import { ExtendFeedsLiveOptions } from './options'
|
||||||
|
import { FollowingListID } from './types'
|
||||||
|
|
||||||
|
const { options } = getComponentSettings<ExtendFeedsLiveOptions>('extendFeedsLive')
|
||||||
|
|
||||||
|
const pinnedListID = ref(options.pinnedListID)
|
||||||
|
const handlePinnedListChange = (value: FollowingListID) => {
|
||||||
|
options.pinnedListID = value
|
||||||
|
pinnedListID.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenListID = ref(options.hiddenListID)
|
||||||
|
const handleHiddenListChange = (value: FollowingListID) => {
|
||||||
|
options.hiddenListID = value
|
||||||
|
hiddenListID.value = value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
.extend-feeds-live-extra-options {
|
||||||
|
margin-top: 8px;
|
||||||
|
@include v-stretch(12px);
|
||||||
|
.extend-feeds-live-extra-option {
|
||||||
|
@include v-stretch(8px);
|
||||||
|
&-header {
|
||||||
|
@include semi-bold();
|
||||||
|
}
|
||||||
|
&-description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<VDropdown
|
||||||
|
:items="followingList"
|
||||||
|
:key-mapper="item => item.id"
|
||||||
|
class="be-live-following-list-select"
|
||||||
|
:value="selectedItem"
|
||||||
|
@change="handleFollowingListChange"
|
||||||
|
>
|
||||||
|
<template #item="{ item }">
|
||||||
|
<span class="be-live-following-list-item"> {{ item.displayName }} </span>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { VDropdown } from '@/ui'
|
||||||
|
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
|
||||||
|
import { FollowingListID, FollowingListItem, RawFollowingListItem } from './types'
|
||||||
|
import { UnselectedListID } from './options'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: FollowingListID
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'change', value: FollowingListID): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const followingListID = computed<FollowingListID>(() => props.value)
|
||||||
|
const handleFollowingListChange = (value: FollowingListItem) => {
|
||||||
|
emit('change', value.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unselectedListItem = reactive<FollowingListItem>({
|
||||||
|
id: UnselectedListID,
|
||||||
|
displayName: '<未选择>',
|
||||||
|
count: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const followingList = ref<FollowingListItem[]>([])
|
||||||
|
const selectedItem = computed(() => {
|
||||||
|
return followingList.value.find(item => item.id === followingListID.value) ?? unselectedListItem
|
||||||
|
})
|
||||||
|
const loadFollowingList = async () => {
|
||||||
|
const list = await bilibiliApi<RawFollowingListItem[]>(
|
||||||
|
getJsonWithCredentials('https://api.bilibili.com/x/relation/tags'),
|
||||||
|
)
|
||||||
|
followingList.value = [
|
||||||
|
unselectedListItem,
|
||||||
|
...list.map(item => ({
|
||||||
|
id: item.tagid,
|
||||||
|
displayName: item.name,
|
||||||
|
count: item.count,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
props.value !== UnselectedListID &&
|
||||||
|
!followingList.value.some(item => item.id === props.value)
|
||||||
|
) {
|
||||||
|
followingList.value.unshift({
|
||||||
|
id: props.value,
|
||||||
|
displayName: '<已删除的分组>',
|
||||||
|
count: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFollowingList()
|
||||||
|
</script>
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="be-live-list-count">({{ items.length }})</div>
|
<div class="be-live-list-count">({{ items.length }})</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="be-live-list-actions">
|
<div class="be-live-list-actions">
|
||||||
<div v-if="loaded" class="be-live-list-refresh" title="刷新" @click="refresh">
|
<div v-if="loaded" class="be-live-list-refresh" title="刷新" @click="fetchLiveList">
|
||||||
<VIcon icon="mdi-refresh" :size="16" />
|
<VIcon icon="mdi-refresh" :size="16" />
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@ -49,21 +49,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { VIcon, TextBox, DpiImage, VEmpty, VLoading } from '@/ui'
|
import { VIcon, TextBox, DpiImage, VEmpty, VLoading } from '@/ui'
|
||||||
import { getJsonWithCredentials, responsiveGetPages } from '@/core/ajax'
|
import { bilibiliApi, getJsonWithCredentials, getPages, responsiveGetPages } from '@/core/ajax'
|
||||||
|
import { FollowingListID, FollowingUserInfo, LiveInfo, RawFollowingListItem } from './types'
|
||||||
interface LiveInfo {
|
import { getUID } from '@/core/utils'
|
||||||
cover: string
|
import { ExtendFeedsLiveOptions, UnselectedListID } from './options'
|
||||||
face: string
|
import {
|
||||||
uname: string
|
addComponentListener,
|
||||||
title: string
|
getComponentSettings,
|
||||||
roomid: number
|
removeComponentListener,
|
||||||
pic: string
|
} from '@/core/settings'
|
||||||
online: number
|
|
||||||
uid: number
|
|
||||||
link: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodeTitle = (title: string) => {
|
const decodeTitle = (title: string) => {
|
||||||
const textArea = document.createElement('textarea')
|
const textArea = document.createElement('textarea')
|
||||||
@ -71,7 +67,11 @@ const decodeTitle = (title: string) => {
|
|||||||
return textArea.value
|
return textArea.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FollowingListPageSize = 500
|
||||||
|
const { options } = getComponentSettings<ExtendFeedsLiveOptions>('extendFeedsLive')
|
||||||
const items = ref<LiveInfo[]>([])
|
const items = ref<LiveInfo[]>([])
|
||||||
|
const pinnedUsers = ref<FollowingUserInfo[]>([])
|
||||||
|
const hiddenUsers = ref<FollowingUserInfo[]>([])
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
|
|
||||||
@ -88,12 +88,13 @@ const filteredItems = computed(() => {
|
|||||||
return items.value
|
return items.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchRecommendItems = async (): Promise<LiveInfo[]> => {
|
const fetchRecommendLiveInfos = async (): Promise<LiveInfo[]> => {
|
||||||
// 动态 portal 接口会获取推荐的 top30 直播。同时这个接口不会忽略悄悄关注的 up 的直播。
|
// 动态 portal 接口会获取推荐的 top30 直播。同时这个接口不会忽略悄悄关注的 up 的直播。
|
||||||
const portalList = await getJsonWithCredentials(
|
const portalList = await getJsonWithCredentials(
|
||||||
'https://api.bilibili.com/x/polymer/web-dynamic/v1/portal',
|
'https://api.bilibili.com/x/polymer/web-dynamic/v1/portal',
|
||||||
)
|
)
|
||||||
const recommendLiveItems = portalList.data.live_users.items.map(item => {
|
const recommendLiveItems =
|
||||||
|
portalList.data.live_users?.items?.map(item => {
|
||||||
const { jump_url, room_id, face, title, uname, mid } = item
|
const { jump_url, room_id, face, title, uname, mid } = item
|
||||||
return {
|
return {
|
||||||
cover: face,
|
cover: face,
|
||||||
@ -106,19 +107,76 @@ const fetchRecommendItems = async (): Promise<LiveInfo[]> => {
|
|||||||
uid: mid,
|
uid: mid,
|
||||||
link: jump_url,
|
link: jump_url,
|
||||||
}
|
}
|
||||||
})
|
}) ?? []
|
||||||
return recommendLiveItems
|
return recommendLiveItems
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByRecommend = (feedItems: LiveInfo[], recommendItems: LiveInfo[]) => {
|
const mergeRecommendLiveInfos = (liveInfos: LiveInfo[], recommendLiveInfos: LiveInfo[]) => {
|
||||||
// recommendItems 里的 pic 和 online 为默认值,但是 ui 也没用到。
|
// recommendItems 里的 pic 和 online 为默认值,但是 ui 也没用到。
|
||||||
// 所以方便起见,直接拼接没出现在 recommendItems 里的 item
|
// 所以方便起见,直接拼接没出现在 recommendItems 里的 item
|
||||||
const recommendRoomIds = recommendItems.map(item => item.roomid)
|
const recommendRoomIds = recommendLiveInfos.map(item => item.roomid)
|
||||||
const feedConcatItems = feedItems.filter(item => !recommendRoomIds.includes(item.roomid))
|
const feedConcatItems = liveInfos.filter(item => !recommendRoomIds.includes(item.roomid))
|
||||||
return lodash.concat(recommendItems, feedConcatItems)
|
return lodash.concat(recommendLiveInfos, feedConcatItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async () => {
|
const sortByUsers = (users: FollowingUserInfo[], descending = false) => {
|
||||||
|
return (a: LiveInfo, b: LiveInfo) => {
|
||||||
|
const aPinnedIndex = users.findIndex(it => it.mid === a.uid)
|
||||||
|
const bPinnedIndex = users.findIndex(it => it.mid === b.uid)
|
||||||
|
if (bPinnedIndex === -1 && aPinnedIndex === -1) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (aPinnedIndex === -1) {
|
||||||
|
return descending ? -1 : 1
|
||||||
|
}
|
||||||
|
if (bPinnedIndex === -1) {
|
||||||
|
return descending ? 1 : -1
|
||||||
|
}
|
||||||
|
return aPinnedIndex - bPinnedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchFollowingLists = async () => {
|
||||||
|
const fetchFollowingList = async (id: FollowingListID) => {
|
||||||
|
if (id === UnselectedListID || id === undefined) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const list = await bilibiliApi<RawFollowingListItem[]>(
|
||||||
|
getJsonWithCredentials('https://api.bilibili.com/x/relation/tags'),
|
||||||
|
)
|
||||||
|
if (!list.some(it => it.tagid === id)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const response = await getPages<FollowingUserInfo>({
|
||||||
|
api: page =>
|
||||||
|
getJsonWithCredentials(
|
||||||
|
`https://api.bilibili.com/x/relation/tag?tagid=${id}&pn=${page}&ps=${FollowingListPageSize}&mid=${getUID()}`,
|
||||||
|
),
|
||||||
|
getList: json => lodash.get(json, 'data', []),
|
||||||
|
getTotal: () => list.find(it => it.tagid === id)?.count ?? 0,
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fetchFollowingList(options.pinnedListID)
|
||||||
|
.then(users => {
|
||||||
|
pinnedUsers.value = users
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
pinnedUsers.value = []
|
||||||
|
}),
|
||||||
|
fetchFollowingList(options.hiddenListID)
|
||||||
|
.then(users => {
|
||||||
|
hiddenUsers.value = users
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
hiddenUsers.value = []
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLiveList = async () => {
|
||||||
try {
|
try {
|
||||||
items.value = []
|
items.value = []
|
||||||
loaded.value = false
|
loaded.value = false
|
||||||
@ -145,15 +203,28 @@ const refresh = async () => {
|
|||||||
getTotal: json => lodash.get(json, 'data.count', 0),
|
getTotal: json => lodash.get(json, 'data.count', 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [allItems, recommendItems] = await Promise.all([promise, fetchRecommendItems()])
|
const [allItems, recommendItems] = await Promise.all([promise, fetchRecommendLiveInfos()])
|
||||||
items.value = sortByRecommend(allItems, recommendItems)
|
const sortedItems = mergeRecommendLiveInfos(allItems, recommendItems)
|
||||||
|
.sort(sortByUsers(pinnedUsers.value, false))
|
||||||
|
.filter(item => !hiddenUsers.value.some(user => user.mid === item.uid))
|
||||||
|
items.value = sortedItems
|
||||||
} finally {
|
} finally {
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initializeLiveList = async () => {
|
||||||
|
await fetchFollowingLists()
|
||||||
|
await fetchLiveList()
|
||||||
|
}
|
||||||
|
addComponentListener('extendFeedsLive.pinnedListID', initializeLiveList)
|
||||||
|
addComponentListener('extendFeedsLive.hiddenListID', initializeLiveList)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refresh()
|
initializeLiveList()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeComponentListener('extendFeedsLive.pinnedListID', initializeLiveList)
|
||||||
|
removeComponentListener('extendFeedsLive.hiddenListID', initializeLiveList)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { styledComponentEntry } from '@/components/styled-component'
|
|||||||
import { select } from '@/core/spin-query'
|
import { select } from '@/core/spin-query'
|
||||||
import { mountVueComponent } from '@/core/utils'
|
import { mountVueComponent } from '@/core/utils'
|
||||||
import { useScopedConsole } from '@/core/utils/log'
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
|
import { options } from './options'
|
||||||
|
|
||||||
const entry = async () => {
|
const entry = async () => {
|
||||||
const console = useScopedConsole('extendFeedsLive')
|
const console = useScopedConsole('extendFeedsLive')
|
||||||
@ -46,4 +47,6 @@ export const component = defineComponentMetadata({
|
|||||||
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
|
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
|
||||||
tags: [componentsTags.feeds, componentsTags.live],
|
tags: [componentsTags.feeds, componentsTags.live],
|
||||||
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
||||||
|
extraOptions: () => import('./ExtraOptions.vue').then(m => m.default),
|
||||||
|
options,
|
||||||
})
|
})
|
||||||
|
|||||||
19
registry/lib/components/feeds/extend-live/options.ts
Normal file
19
registry/lib/components/feeds/extend-live/options.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineOptionsMetadata, OptionsOfMetadata } from '@/components/define'
|
||||||
|
import { FollowingListID } from './types'
|
||||||
|
|
||||||
|
export const UnselectedListID: FollowingListID = -1
|
||||||
|
|
||||||
|
export const options = defineOptionsMetadata({
|
||||||
|
pinnedListID: {
|
||||||
|
defaultValue: UnselectedListID as FollowingListID,
|
||||||
|
displayName: '置顶列表 ID',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
hiddenListID: {
|
||||||
|
defaultValue: UnselectedListID as FollowingListID,
|
||||||
|
displayName: '隐藏列表 ID',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ExtendFeedsLiveOptions = OptionsOfMetadata<typeof options>
|
||||||
69
registry/lib/components/feeds/extend-live/types.ts
Normal file
69
registry/lib/components/feeds/extend-live/types.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
export type FollowingListID = number
|
||||||
|
|
||||||
|
export interface LiveInfo {
|
||||||
|
cover: string
|
||||||
|
face: string
|
||||||
|
uname: string
|
||||||
|
title: string
|
||||||
|
roomid: number
|
||||||
|
pic: string
|
||||||
|
online: number
|
||||||
|
uid: number
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FollowingUserInfo {
|
||||||
|
mid: number
|
||||||
|
attribute: number
|
||||||
|
special: number
|
||||||
|
contract_info: Record<string, unknown>
|
||||||
|
uname: string
|
||||||
|
face: string
|
||||||
|
sign: string
|
||||||
|
face_nft: number
|
||||||
|
official_verify: {
|
||||||
|
type: number
|
||||||
|
desc: string
|
||||||
|
}
|
||||||
|
vip: {
|
||||||
|
vipType: number
|
||||||
|
vipDueDate: number
|
||||||
|
dueRemark: string
|
||||||
|
accessStatus: number
|
||||||
|
vipStatus: number
|
||||||
|
vipStatusWarn: string
|
||||||
|
themeType: number
|
||||||
|
label: {
|
||||||
|
path: string
|
||||||
|
text: string
|
||||||
|
label_theme: string
|
||||||
|
text_color: string
|
||||||
|
bg_style: number
|
||||||
|
bg_color: string
|
||||||
|
border_color: string
|
||||||
|
}
|
||||||
|
avatar_subscript: number
|
||||||
|
nickname_color: string
|
||||||
|
avatar_subscript_url: string
|
||||||
|
}
|
||||||
|
name_render: Record<string, unknown>
|
||||||
|
live: {
|
||||||
|
live_status: number
|
||||||
|
jump_url: string
|
||||||
|
}
|
||||||
|
nft_icon: string
|
||||||
|
rec_reason: string
|
||||||
|
track_id: string
|
||||||
|
follow_time: string
|
||||||
|
}
|
||||||
|
export interface RawFollowingListItem {
|
||||||
|
tagid: number
|
||||||
|
name: string
|
||||||
|
count: number
|
||||||
|
tip: string
|
||||||
|
}
|
||||||
|
export interface FollowingListItem {
|
||||||
|
id: FollowingListID
|
||||||
|
displayName: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
156
registry/lib/components/feeds/group-filter/FilterPanel.vue
Normal file
156
registry/lib/components/feeds/group-filter/FilterPanel.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div class="group-filter-panel">
|
||||||
|
<div class="group-filter-header">
|
||||||
|
<h1>分组</h1>
|
||||||
|
<switch-box v-model="allChecked" />
|
||||||
|
</div>
|
||||||
|
<div v-for="(group, index) in groups" :key="index" class="group-item">
|
||||||
|
<CheckBox v-model="group.checked">
|
||||||
|
{{ group.name }}
|
||||||
|
</CheckBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { CheckBox, SwitchBox } from '@/ui'
|
||||||
|
import { bilibiliApi, getJsonWithCredentials, getPages } from '@/core/ajax'
|
||||||
|
import { FeedsCard, forEachFeedsCard } from '@/components/feeds/api'
|
||||||
|
import { getUID } from '@/core/utils'
|
||||||
|
|
||||||
|
interface Group {
|
||||||
|
name: string
|
||||||
|
checked: boolean
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let cardsManager: typeof import('@/components/feeds/api').feedsCardsManager
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
SwitchBox,
|
||||||
|
CheckBox,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groups: [],
|
||||||
|
followingMap: new Map<string, number[]>(), // username -> tagid[]
|
||||||
|
selectedGroupIds: [],
|
||||||
|
allChecked: true,
|
||||||
|
} as {
|
||||||
|
groups: Group[]
|
||||||
|
followingMap: Map<string, number[]>
|
||||||
|
selectedGroupIds: number[]
|
||||||
|
allChecked: boolean
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
groups: {
|
||||||
|
handler(newGroups: Group[]) {
|
||||||
|
this.selectedGroupIds = newGroups.filter(group => group.checked).map(group => group.id)
|
||||||
|
cardsManager.cards.forEach(card => {
|
||||||
|
this.updateCard(lodash.clone(card))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
allChecked: {
|
||||||
|
handler(newChecked: boolean) {
|
||||||
|
for (const group of this.groups) {
|
||||||
|
group.checked = newChecked
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
cardsManager = await forEachFeedsCard({
|
||||||
|
added: card => {
|
||||||
|
this.updateCard(lodash.clone(card))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// fetch groups
|
||||||
|
this.groups = await bilibiliApi<Array<any>>(
|
||||||
|
getJsonWithCredentials('https://api.bilibili.com/x/relation/tags'),
|
||||||
|
'分组信息获取失败',
|
||||||
|
).then(res =>
|
||||||
|
res.map(value => ({
|
||||||
|
name: value.name,
|
||||||
|
checked: true,
|
||||||
|
id: value.tagid,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const uid = getUID()
|
||||||
|
// fetch following
|
||||||
|
const allPages = await getPages({
|
||||||
|
api: page =>
|
||||||
|
getJsonWithCredentials(
|
||||||
|
`https://api.bilibili.com/x/relation/followings?vmid=${uid}&pn=${page}&ps=50`,
|
||||||
|
),
|
||||||
|
getList: json => json.data.list,
|
||||||
|
getTotal: json => json.data.total,
|
||||||
|
})
|
||||||
|
allPages.forEach(user => {
|
||||||
|
this.followingMap.set(user.uname, user.tag)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateCard(card: FeedsCard) {
|
||||||
|
// 从username获得tag
|
||||||
|
const userTagIds: number[] = this.followingMap.get(card.username)
|
||||||
|
if (!userTagIds || !this.selectedGroupIds) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!userTagIds.some(item => this.selectedGroupIds.includes(item))) {
|
||||||
|
card.element.classList.add('group-filter-hide-feed')
|
||||||
|
} else {
|
||||||
|
card.element.classList.remove('group-filter-hide-feed')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.group-filter-hide-feed {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-filter-panel {
|
||||||
|
background-color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
.group-filter-header {
|
||||||
|
cursor: pointer;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark & {
|
||||||
|
color: #eee;
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
registry/lib/components/feeds/group-filter/index.md
Normal file
1
registry/lib/components/feeds/group-filter/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
按照关注分组筛选动态
|
||||||
34
registry/lib/components/feeds/group-filter/index.ts
Normal file
34
registry/lib/components/feeds/group-filter/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
import { feedsCardsManager } from '@/components/feeds/api'
|
||||||
|
|
||||||
|
const entry = async () => {
|
||||||
|
const { select } = await import('@/core/spin-query')
|
||||||
|
let leftPanel: HTMLElement
|
||||||
|
if (feedsCardsManager.managerType === 'v2') {
|
||||||
|
const leftAside = await select('.bili-dyn-home--member aside.left')
|
||||||
|
const section = document.createElement('section')
|
||||||
|
section.classList.add('group-filter-section')
|
||||||
|
leftAside.insertAdjacentElement('afterbegin', section)
|
||||||
|
leftPanel = section
|
||||||
|
} else {
|
||||||
|
leftPanel = await select('.home-container .left-panel')
|
||||||
|
}
|
||||||
|
if (leftPanel === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const FilterPanel = await import('./FilterPanel.vue')
|
||||||
|
const { mountVueComponent } = await import('@/core/utils')
|
||||||
|
leftPanel.insertAdjacentElement('afterbegin', mountVueComponent(FilterPanel).$el)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'feedsGroupFilter',
|
||||||
|
entry,
|
||||||
|
displayName: '动态分组过滤',
|
||||||
|
author: { name: 'Rinne', link: 'https://github.com/OharaRinneY' },
|
||||||
|
urlInclude: [
|
||||||
|
// 仅动态首页
|
||||||
|
/^https:\/\/t\.bilibili\.com\/$/,
|
||||||
|
],
|
||||||
|
tags: [componentsTags.feeds],
|
||||||
|
})
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">一键点亮粉丝勋章</p>
|
||||||
|
<TextBox
|
||||||
|
placeholder="直播间ID"
|
||||||
|
:text="roomid"
|
||||||
|
@change="handleRoomIdChange"
|
||||||
|
:changeOnBlur="true"
|
||||||
|
></TextBox>
|
||||||
|
<AsyncButton @click="handleKeepAliveRequest">点亮!</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { TextBox, AsyncButton } from '@/ui'
|
||||||
|
|
||||||
|
import { validateRoomId, getLiveRoomId, keepAliveRequest } from './utils'
|
||||||
|
import { Toast } from '@/core/toast'
|
||||||
|
|
||||||
|
const roomid = ref(getLiveRoomId())
|
||||||
|
const handleRoomIdChange = (value: string) => {
|
||||||
|
if (validateRoomId(value)) {
|
||||||
|
roomid.value = value
|
||||||
|
} else {
|
||||||
|
roomid.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeepAliveRequest = async () => {
|
||||||
|
if (!roomid.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await keepAliveRequest(roomid.value)
|
||||||
|
Toast.success('发送点亮勋章请求成功', '提示')
|
||||||
|
} catch ({ message }) {
|
||||||
|
Toast.error(`勋章点亮失败,原因: ${message}`, '提示')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
|
||||||
|
box-shadow: 0 0 0 1px #8884;
|
||||||
|
order: -2;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 6px 6px 10px;
|
||||||
|
@include v-stretch(6px);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include semi-bold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
registry/lib/components/live/badge-keepalive/index.md
Normal file
1
registry/lib/components/live/badge-keepalive/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
在直播间页面的功能面板添加一键点亮粉丝勋章功能,仅适用于有粉丝勋章且正在直播的直播间。原理:发送一个 300 次点赞的请求点亮粉丝勋章。
|
||||||
20
registry/lib/components/live/badge-keepalive/index.ts
Normal file
20
registry/lib/components/live/badge-keepalive/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
import { getUID } from '@/core/utils'
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'badgeKeepalive',
|
||||||
|
displayName: '一键点亮直播间粉丝勋章',
|
||||||
|
entry: none,
|
||||||
|
reload: none,
|
||||||
|
unload: none,
|
||||||
|
author: {
|
||||||
|
name: 'magicFeirl',
|
||||||
|
link: 'https://github.com/magicFeirl',
|
||||||
|
},
|
||||||
|
tags: [componentsTags.live],
|
||||||
|
widget: {
|
||||||
|
component: () => import('./BadgeKeepalive.vue').then(m => m.default),
|
||||||
|
condition: () => Boolean(getUID()),
|
||||||
|
},
|
||||||
|
urlInclude: ['//live.bilibili.com'],
|
||||||
|
})
|
||||||
55
registry/lib/components/live/badge-keepalive/utils.ts
Normal file
55
registry/lib/components/live/badge-keepalive/utils.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { postTextWithCredentials, getJsonWithCredentials } from '@/core/ajax'
|
||||||
|
import { getCsrf, getUID } from '@/core/utils'
|
||||||
|
|
||||||
|
// 获取当前直播间号
|
||||||
|
export function getLiveRoomId(): string {
|
||||||
|
const matched = location.href.match(/live.bilibili.com\/(\d+)/)
|
||||||
|
return matched ? matched[1] : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRoomId(value: string): boolean {
|
||||||
|
return /^\d+$/.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateJSON(data) {
|
||||||
|
if (data.code !== 0) {
|
||||||
|
throw new Error(data.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLiveRoomUserInfo(room_id: string) {
|
||||||
|
const data = await getJsonWithCredentials(
|
||||||
|
`https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByUser?room_id=${room_id}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return validateJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function keepAliveRequest(room_id: string, click_time = '300') {
|
||||||
|
// 需要先获取直播间房主的 UID
|
||||||
|
const data = await getLiveRoomUserInfo(room_id)
|
||||||
|
|
||||||
|
const { curr_weared } = data.medal
|
||||||
|
if (!curr_weared) {
|
||||||
|
throw new Error(`暂未获得直播间 ${room_id} 的粉丝勋章`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchor_id = curr_weared.target_id
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
click_time,
|
||||||
|
room_id,
|
||||||
|
anchor_id,
|
||||||
|
uid: getUID(),
|
||||||
|
csrf: getCsrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseURL =
|
||||||
|
'https://api.live.bilibili.com/xlive/app-ucenter/v1/like_info_v3/like/likeReportV3'
|
||||||
|
|
||||||
|
return validateJSON(
|
||||||
|
JSON.parse(await postTextWithCredentials(baseURL, new URLSearchParams(params))),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,9 +4,10 @@ import { ranking } from './ranking/ranking'
|
|||||||
import { userInfo } from './user-info/user-info'
|
import { userInfo } from './user-info/user-info'
|
||||||
import { logo } from './logo/logo'
|
import { logo } from './logo/logo'
|
||||||
import { home } from './home/home'
|
import { home } from './home/home'
|
||||||
import { gamesIframe, livesIframe, mangaIframe } from './iframe/iframe'
|
import { games } from './games/games'
|
||||||
import { blanks } from './flexible-blank/flexible-blank'
|
import { blanks } from './flexible-blank/flexible-blank'
|
||||||
import { bangumi, music, shop, match, creations } from './simple-links/simple-links'
|
import { bangumi, music, shop, creations, lives } from './simple-links/simple-links'
|
||||||
|
import { match } from './match/match'
|
||||||
import { upload } from './upload/upload'
|
import { upload } from './upload/upload'
|
||||||
import { search } from './search/search'
|
import { search } from './search/search'
|
||||||
import { feeds } from './feeds/feeds'
|
import { feeds } from './feeds/feeds'
|
||||||
@ -14,6 +15,7 @@ import { subscriptions } from './subscriptions/subscriptions'
|
|||||||
import { watchlater } from './watchlater/watchlater'
|
import { watchlater } from './watchlater/watchlater'
|
||||||
import { favorites } from './favorites/favorites'
|
import { favorites } from './favorites/favorites'
|
||||||
import { history } from './history/history'
|
import { history } from './history/history'
|
||||||
|
import { manga } from './manga/manga'
|
||||||
|
|
||||||
const [blank1, blank2, blank3, blank4] = blanks
|
const [blank1, blank2, blank3, blank4] = blanks
|
||||||
export const getBuiltInItems = (): CustomNavbarItemInit[] => [
|
export const getBuiltInItems = (): CustomNavbarItemInit[] => [
|
||||||
@ -23,11 +25,11 @@ export const getBuiltInItems = (): CustomNavbarItemInit[] => [
|
|||||||
bangumi,
|
bangumi,
|
||||||
ranking,
|
ranking,
|
||||||
music,
|
music,
|
||||||
gamesIframe,
|
games,
|
||||||
livesIframe,
|
lives,
|
||||||
shop,
|
shop,
|
||||||
match,
|
match,
|
||||||
mangaIframe,
|
manga,
|
||||||
blank2,
|
blank2,
|
||||||
search,
|
search,
|
||||||
blank3,
|
blank3,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { addComponentListener } from '@/core/settings'
|
|||||||
import { isIframe, isNotHtml, matchUrlPattern, mountVueComponent } from '@/core/utils'
|
import { isIframe, isNotHtml, matchUrlPattern, mountVueComponent } from '@/core/utils'
|
||||||
import { setupNotifyStyle } from './notify-style'
|
import { setupNotifyStyle } from './notify-style'
|
||||||
import { setupLinkPopupContentAlignStyle } from './link-popup-content-align-style'
|
import { setupLinkPopupContentAlignStyle } from './link-popup-content-align-style'
|
||||||
|
import { migrateOrders } from './orders-migration'
|
||||||
|
|
||||||
export const entry: ComponentEntry = async ({ metadata: { name } }) => {
|
export const entry: ComponentEntry = async ({ metadata: { name } }) => {
|
||||||
// const url = document.URL.replace(location.search, '')
|
// const url = document.URL.replace(location.search, '')
|
||||||
@ -51,6 +52,7 @@ export const entry: ComponentEntry = async ({ metadata: { name } }) => {
|
|||||||
if (matchUrlPattern('https://www.bilibili.com/account/history')) {
|
if (matchUrlPattern('https://www.bilibili.com/account/history')) {
|
||||||
document.body.classList.add('history-page')
|
document.body.classList.add('history-page')
|
||||||
}
|
}
|
||||||
|
migrateOrders()
|
||||||
const CustomNavbar = await import('./CustomNavbar.vue')
|
const CustomNavbar = await import('./CustomNavbar.vue')
|
||||||
const customNavbar: Vue & {
|
const customNavbar: Vue & {
|
||||||
styles: string[]
|
styles: string[]
|
||||||
|
|||||||
179
registry/lib/components/style/custom-navbar/games/GamesPopup.vue
Normal file
179
registry/lib/components/style/custom-navbar/games/GamesPopup.vue
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VLoading } from '@/ui'
|
||||||
|
import type { GameInfo } from './types'
|
||||||
|
import { bilibiliApi, getJson } from '@/core/ajax'
|
||||||
|
import { usePopper } from '../mixins'
|
||||||
|
import { CustomNavbarItem } from '../custom-navbar-item'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: CustomNavbarItem
|
||||||
|
container: HTMLElement
|
||||||
|
}>()
|
||||||
|
const popper = usePopper(props)
|
||||||
|
|
||||||
|
const gameInfo = ref<GameInfo | undefined>(undefined)
|
||||||
|
const loading = ref(true)
|
||||||
|
const loadGameInfo = async () => {
|
||||||
|
if (gameInfo.value !== undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const response = await bilibiliApi<GameInfo>(
|
||||||
|
getJson('https://le3-api.game.bilibili.com/pc/game/nav/info'),
|
||||||
|
)
|
||||||
|
gameInfo.value = response
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
popupShow() {
|
||||||
|
popper.popupShow()
|
||||||
|
loadGameInfo()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="custom-navbar-games-popup">
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<VLoading />
|
||||||
|
</div>
|
||||||
|
<div v-if="gameInfo" class="custom-navbar-games-panel">
|
||||||
|
<div class="custom-navbar-games-left-panel">
|
||||||
|
<a class="banner-game-card" :href="gameInfo.banner.href" :title="gameInfo.banner.title">
|
||||||
|
<img :src="gameInfo.banner.poster" :alt="gameInfo.banner.title" />
|
||||||
|
<div class="banner-game-card-title">
|
||||||
|
{{ gameInfo.banner.title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="panel-game-cards">
|
||||||
|
<a
|
||||||
|
v-for="card of gameInfo.panel"
|
||||||
|
:key="card.href"
|
||||||
|
class="panel-game-card"
|
||||||
|
:href="card.href"
|
||||||
|
:title="card.title"
|
||||||
|
>
|
||||||
|
<img :src="card.poster" :alt="card.title" />
|
||||||
|
<div class="panel-game-card-title">
|
||||||
|
{{ card.title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="custom-navbar-games-panel-separator"></div>
|
||||||
|
<div class="custom-navbar-games-right-panel">
|
||||||
|
<div class="list-game-cards-title">新游预告</div>
|
||||||
|
<div class="list-game-cards">
|
||||||
|
<a
|
||||||
|
v-for="card of gameInfo.list.slice(0, 8)"
|
||||||
|
:key="card.href"
|
||||||
|
class="list-game-card"
|
||||||
|
:href="card.href"
|
||||||
|
:title="card.title"
|
||||||
|
>
|
||||||
|
{{ card.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
.custom-navbar-games-popup {
|
||||||
|
width: 420px;
|
||||||
|
font-size: 12px;
|
||||||
|
$gap: 12px;
|
||||||
|
@include v-stretch();
|
||||||
|
.loading-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
@include v-center();
|
||||||
|
justify-content: center;
|
||||||
|
height: 240px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.custom-navbar-games-panel {
|
||||||
|
@include h-stretch($gap);
|
||||||
|
padding: $gap;
|
||||||
|
}
|
||||||
|
.custom-navbar-games-left-panel {
|
||||||
|
flex-basis: 60%;
|
||||||
|
min-width: 0;
|
||||||
|
@include v-stretch($gap);
|
||||||
|
.banner-game-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #8882;
|
||||||
|
@include v-stretch();
|
||||||
|
@include round-corner();
|
||||||
|
img {
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
aspect-ratio: 15 / 8;
|
||||||
|
}
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
background-image: linear-gradient(to top, #0008, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.panel-game-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: $gap;
|
||||||
|
.panel-game-card {
|
||||||
|
@include v-stretch(6px);
|
||||||
|
min-width: 0;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
img {
|
||||||
|
background-color: #8882;
|
||||||
|
border-radius: 16px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-color) !important;
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
@include max-line(2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-navbar-games-right-panel {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
@include v-stretch($gap);
|
||||||
|
padding: 6px 4px;
|
||||||
|
.list-game-cards-title {
|
||||||
|
font-size: large;
|
||||||
|
@include semi-bold();
|
||||||
|
}
|
||||||
|
.list-game-cards {
|
||||||
|
@include v-stretch();
|
||||||
|
.list-game-card {
|
||||||
|
@include single-line();
|
||||||
|
padding: 4px 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-color) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-navbar-games-panel-separator {
|
||||||
|
width: 1px;
|
||||||
|
background-color: #8882;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
registry/lib/components/style/custom-navbar/games/games.ts
Normal file
14
registry/lib/components/style/custom-navbar/games/games.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CustomNavbarItemInit } from '../custom-navbar-item'
|
||||||
|
|
||||||
|
export const games: CustomNavbarItemInit = {
|
||||||
|
name: 'games',
|
||||||
|
displayName: '游戏中心',
|
||||||
|
content: '游戏中心',
|
||||||
|
|
||||||
|
touch: true,
|
||||||
|
href: 'https://game.bilibili.com/',
|
||||||
|
|
||||||
|
boundingWidth: 420,
|
||||||
|
noPopupPadding: true,
|
||||||
|
popupContent: () => import('./GamesPopup.vue').then(m => m.default),
|
||||||
|
}
|
||||||
11
registry/lib/components/style/custom-navbar/games/types.ts
Normal file
11
registry/lib/components/style/custom-navbar/games/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface GameListItem {
|
||||||
|
href: string
|
||||||
|
title: string
|
||||||
|
poster: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameInfo {
|
||||||
|
panel: GameListItem[]
|
||||||
|
list: GameListItem[]
|
||||||
|
banner: GameListItem
|
||||||
|
}
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<iframe :src="item.src" frameborder="0" :width="item.width" :height="item.height"></iframe>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import type { PropType } from 'vue'
|
|
||||||
import type { NavbarIframeConfig } from './iframe'
|
|
||||||
import { popperMixin } from '../mixins'
|
|
||||||
import { CustomNavbarItem } from '../custom-navbar-item'
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'IframePopup',
|
|
||||||
mixins: [popperMixin],
|
|
||||||
props: {
|
|
||||||
item: {
|
|
||||||
type: CustomNavbarItem as unknown as PropType<CustomNavbarItem & NavbarIframeConfig>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { CustomNavbarItemInit } from '../custom-navbar-item'
|
|
||||||
|
|
||||||
export interface NavbarIframeConfig {
|
|
||||||
src: string
|
|
||||||
href: string
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
lazy: boolean
|
|
||||||
displayName: string
|
|
||||||
iframeName: string
|
|
||||||
}
|
|
||||||
const getIframeItem = (config: NavbarIframeConfig): CustomNavbarItemInit & NavbarIframeConfig => ({
|
|
||||||
...config,
|
|
||||||
name: `${config.iframeName}Iframe`,
|
|
||||||
content: config.displayName,
|
|
||||||
|
|
||||||
touch: true,
|
|
||||||
|
|
||||||
popupContent: () => import('./IframePopup.vue').then(m => m.default),
|
|
||||||
boundingWidth: config.width,
|
|
||||||
noPopupPadding: true,
|
|
||||||
transparentPopup: true,
|
|
||||||
})
|
|
||||||
export const gamesIframe = getIframeItem({
|
|
||||||
src: 'https://www.bilibili.com/page-proxy/game-nav.html',
|
|
||||||
href: 'https://game.bilibili.com/',
|
|
||||||
width: 680,
|
|
||||||
height: 260,
|
|
||||||
lazy: true,
|
|
||||||
displayName: '游戏中心',
|
|
||||||
iframeName: 'games',
|
|
||||||
})
|
|
||||||
export const livesIframe = getIframeItem({
|
|
||||||
src: 'https://live.bilibili.com/blackboard/dropdown-menu.html',
|
|
||||||
href: 'https://live.bilibili.com',
|
|
||||||
width: 528,
|
|
||||||
height: 266,
|
|
||||||
lazy: true,
|
|
||||||
displayName: '直播',
|
|
||||||
iframeName: 'lives',
|
|
||||||
})
|
|
||||||
export const mangaIframe = getIframeItem({
|
|
||||||
src: 'https://manga.bilibili.com/eden/bilibili-nav-panel.html',
|
|
||||||
href: 'https://manga.bilibili.com',
|
|
||||||
width: 720,
|
|
||||||
height: 266,
|
|
||||||
lazy: true,
|
|
||||||
displayName: '漫画',
|
|
||||||
iframeName: 'manga',
|
|
||||||
})
|
|
||||||
@ -60,6 +60,10 @@ const options = defineOptionsMetadata({
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
displayName: '使用季节 Logo',
|
displayName: '使用季节 Logo',
|
||||||
},
|
},
|
||||||
|
themeLogo: {
|
||||||
|
defaultValue: true,
|
||||||
|
displayName: '使用主题色 Logo',
|
||||||
|
},
|
||||||
touch: {
|
touch: {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
displayName: '触摸模式',
|
displayName: '触摸模式',
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="custom-navbar-logo-container">
|
<div class="custom-navbar-logo-container">
|
||||||
<VIcon v-if="!seasonLogoUrl" icon="logo" class="custom-navbar-logo"></VIcon>
|
<VIcon
|
||||||
|
v-if="!seasonLogoUrl"
|
||||||
|
icon="logo"
|
||||||
|
class="custom-navbar-logo"
|
||||||
|
:class="{ theme: useThemeColor }"
|
||||||
|
></VIcon>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="custom-navbar-logo season"
|
class="custom-navbar-logo season"
|
||||||
@ -22,6 +27,7 @@ export default Vue.extend({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
seasonLogoUrl: '',
|
seasonLogoUrl: '',
|
||||||
|
useThemeColor: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -46,6 +52,13 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
addComponentListener(
|
||||||
|
'customNavbar.themeLogo',
|
||||||
|
async (value: boolean) => {
|
||||||
|
this.useThemeColor = value
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -63,9 +76,15 @@ export default Vue.extend({
|
|||||||
width: calc(var(--navbar-height) * 1.3);
|
width: calc(var(--navbar-height) * 1.3);
|
||||||
}
|
}
|
||||||
.custom-navbar-logo {
|
.custom-navbar-logo {
|
||||||
.custom-navbar:not(.fill) & {
|
.custom-navbar:not(.fill) &:not(.season) {
|
||||||
&:not(.season) {
|
body.dark & {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.theme {
|
||||||
color: var(--theme-color);
|
color: var(--theme-color);
|
||||||
|
body.dark & {
|
||||||
|
color: var(--theme-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.be-icon {
|
&.be-icon {
|
||||||
|
|||||||
231
registry/lib/components/style/custom-navbar/manga/MangaPopup.vue
Normal file
231
registry/lib/components/style/custom-navbar/manga/MangaPopup.vue
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { VLoading } from '@/ui'
|
||||||
|
import { bilibiliApi } from '@/core/ajax'
|
||||||
|
import { CustomNavbarItem } from '../custom-navbar-item'
|
||||||
|
import { usePopper } from '../mixins'
|
||||||
|
import type { MangaRecommendItem, MangaHotItem } from './types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: CustomNavbarItem
|
||||||
|
container: HTMLElement
|
||||||
|
}>()
|
||||||
|
const popper = usePopper(props)
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const recommendItems = ref<MangaRecommendItem[] | undefined>(undefined)
|
||||||
|
const hotItems = ref<MangaHotItem[] | undefined>(undefined)
|
||||||
|
const previewCard = ref<MangaHotItem | undefined>(undefined)
|
||||||
|
const loadRecommendItems = async () => {
|
||||||
|
if (recommendItems.value !== undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recommendItems.value = await bilibiliApi<MangaRecommendItem[]>(
|
||||||
|
fetch('https://manga.bilibili.com/twirp/comic.v1.Comic/Recommend?device=pc&platform=web', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
JsonData: JSON.stringify([
|
||||||
|
{
|
||||||
|
pool_id: 500003,
|
||||||
|
num: 4,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}).then(r => r.json()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const loadHotItems = async () => {
|
||||||
|
if (hotItems.value !== undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hotItems.value = await bilibiliApi<MangaHotItem[]>(
|
||||||
|
fetch('https://manga.bilibili.com/twirp/comic.v1.Comic/HomeHot?device=pc&platform=web', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ type: 0 }),
|
||||||
|
method: 'POST',
|
||||||
|
}).then(r => r.json()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const loadMangaInfo = async () => {
|
||||||
|
await Promise.all([loadRecommendItems(), loadHotItems()])
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
popupShow() {
|
||||||
|
popper.popupShow()
|
||||||
|
loadMangaInfo()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="custom-navbar-manga-popup">
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<VLoading />
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading" class="custom-navbar-manga-panel">
|
||||||
|
<div class="custom-navbar-manga-left-panel">
|
||||||
|
<a
|
||||||
|
v-for="card of recommendItems"
|
||||||
|
:key="card.season_id"
|
||||||
|
class="recommend-manga-card"
|
||||||
|
:href="'https://manga.bilibili.com/detail/mc' + card.season_id"
|
||||||
|
>
|
||||||
|
<div class="recommend-manga-card-image">
|
||||||
|
<img :src="card.horizontal_cover + '@300w.jpg'" :alt="card.title" />
|
||||||
|
</div>
|
||||||
|
<div class="recommend-manga-card-title" :title="card.title">{{ card.title }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="custom-navbar-manga-panel-separator"></div>
|
||||||
|
<div class="custom-navbar-manga-right-panel">
|
||||||
|
<div class="custom-navbar-manga-hot-title">人气漫画</div>
|
||||||
|
<div class="custom-navbar-manga-hot-list">
|
||||||
|
<a
|
||||||
|
v-for="card of hotItems.slice(0, 7)"
|
||||||
|
:key="card.comic_id"
|
||||||
|
class="hot-manga-card"
|
||||||
|
:title="card.title"
|
||||||
|
:href="'https://manga.bilibili.com/detail/mc' + card.comic_id"
|
||||||
|
@mouseenter="previewCard = card"
|
||||||
|
@mouseleave="previewCard = undefined"
|
||||||
|
>
|
||||||
|
<div class="hot-manga-card-title">
|
||||||
|
{{ card.title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="previewCard" class="hot-manga-card-preview">
|
||||||
|
<img :src="previewCard.vertical_cover + '@450h.jpg'" :alt="previewCard.title" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
.custom-navbar-manga-popup {
|
||||||
|
width: 500px;
|
||||||
|
font-size: 12px;
|
||||||
|
$gap: 12px;
|
||||||
|
@include v-stretch();
|
||||||
|
.loading-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
@include v-center();
|
||||||
|
justify-content: center;
|
||||||
|
height: 240px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.custom-navbar-manga-panel {
|
||||||
|
@include h-stretch($gap);
|
||||||
|
padding: $gap;
|
||||||
|
.custom-navbar-manga-left-panel {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 65%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: $gap;
|
||||||
|
.recommend-manga-card {
|
||||||
|
@include v-stretch(8px);
|
||||||
|
.recommend-manga-card-image {
|
||||||
|
@include v-stretch();
|
||||||
|
@include round-corner();
|
||||||
|
background: #8882;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
@include single-line();
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-navbar-manga-right-panel {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
@include v-stretch($gap);
|
||||||
|
padding: 6px 4px;
|
||||||
|
.custom-navbar-manga-hot-title {
|
||||||
|
font-size: large;
|
||||||
|
@include semi-bold();
|
||||||
|
}
|
||||||
|
.custom-navbar-manga-hot-list {
|
||||||
|
@include v-stretch();
|
||||||
|
counter-reset: manga-item;
|
||||||
|
.hot-manga-card {
|
||||||
|
@include h-center(6px);
|
||||||
|
padding: 4px 0;
|
||||||
|
&-title {
|
||||||
|
@include single-line();
|
||||||
|
transition: color 0.2s ease-out, opacity 0.2s ease-out;
|
||||||
|
opacity: 0.6;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
counter-increment: manga-item;
|
||||||
|
content: counter(manga-item);
|
||||||
|
@include semi-bold();
|
||||||
|
opacity: 0.6;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
&:hover .hot-manga-card-title {
|
||||||
|
color: var(--theme-color) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:nth-child(1)::before {
|
||||||
|
opacity: 1;
|
||||||
|
color: #e64747;
|
||||||
|
}
|
||||||
|
&:nth-child(2)::before {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ff6e42;
|
||||||
|
}
|
||||||
|
&:nth-child(3)::before {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ff9900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hot-manga-card-preview {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: calc(-1 * #{$gap});
|
||||||
|
right: calc(100% + #{$gap} + 1px);
|
||||||
|
height: calc(100% - #{$gap});
|
||||||
|
background-image: linear-gradient(to left, #fff 75%, transparent);
|
||||||
|
padding: 18px 18px 18px 60px;
|
||||||
|
body.dark & {
|
||||||
|
background-image: linear-gradient(to left, #222 75%, transparent);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: flex;
|
||||||
|
background: #8882;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 169 / 225;
|
||||||
|
@include round-corner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-navbar-manga-panel-separator {
|
||||||
|
width: 1px;
|
||||||
|
background-color: #8882;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
registry/lib/components/style/custom-navbar/manga/manga.ts
Normal file
14
registry/lib/components/style/custom-navbar/manga/manga.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CustomNavbarItemInit } from '../custom-navbar-item'
|
||||||
|
|
||||||
|
export const manga: CustomNavbarItemInit = {
|
||||||
|
name: 'manga',
|
||||||
|
displayName: '漫画',
|
||||||
|
content: '漫画',
|
||||||
|
|
||||||
|
touch: true,
|
||||||
|
href: 'https://manga.bilibili.com/',
|
||||||
|
|
||||||
|
boundingWidth: 500,
|
||||||
|
noPopupPadding: true,
|
||||||
|
popupContent: () => import('./MangaPopup.vue').then(m => m.default),
|
||||||
|
}
|
||||||
52
registry/lib/components/style/custom-navbar/manga/types.ts
Normal file
52
registry/lib/components/style/custom-navbar/manga/types.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export interface MangaRecommendItem {
|
||||||
|
season_id: number
|
||||||
|
title: string
|
||||||
|
horizontal_cover: string
|
||||||
|
square_cover: string
|
||||||
|
vertical_cover: string
|
||||||
|
is_finish: number
|
||||||
|
status: number
|
||||||
|
last_ord: number
|
||||||
|
total: number
|
||||||
|
release_time: string
|
||||||
|
last_short_title: string
|
||||||
|
ext: string
|
||||||
|
recommend_reason: string
|
||||||
|
discount_type: number
|
||||||
|
allow_wait_free: boolean
|
||||||
|
styles: string[]
|
||||||
|
tags: string[]
|
||||||
|
rank_info: {
|
||||||
|
name: string
|
||||||
|
rank: number
|
||||||
|
}
|
||||||
|
score: number
|
||||||
|
update_day: number
|
||||||
|
good_review: number
|
||||||
|
search_num: number
|
||||||
|
author_name: string[]
|
||||||
|
rec_tag_id: number
|
||||||
|
rec_tag: string
|
||||||
|
rec_tag_type: number
|
||||||
|
vertical_cover_info: {
|
||||||
|
cover_id: string
|
||||||
|
cover_type: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaHotItem {
|
||||||
|
comic_id: number
|
||||||
|
title: string
|
||||||
|
author: string[]
|
||||||
|
vertical_cover: string
|
||||||
|
is_finish: number
|
||||||
|
last_ord: number
|
||||||
|
last_short_title: string
|
||||||
|
styles: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
total: number
|
||||||
|
last_rank: number
|
||||||
|
last_ep: number
|
||||||
|
}
|
||||||
252
registry/lib/components/style/custom-navbar/match/MatchPopup.vue
Normal file
252
registry/lib/components/style/custom-navbar/match/MatchPopup.vue
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { VLoading, VButton } from '@/ui'
|
||||||
|
import { bilibiliApi, getJsonWithCredentials, postTextWithCredentials } from '@/core/ajax'
|
||||||
|
import { CustomNavbarItem } from '../custom-navbar-item'
|
||||||
|
import { usePopper } from '../mixins'
|
||||||
|
import { type MatchInfo, type MatchPreviewItem, MatchHotItemStatus } from './types'
|
||||||
|
import { getCsrf } from '@/core/utils'
|
||||||
|
import { logError } from '@/core/utils/log'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: CustomNavbarItem
|
||||||
|
container: HTMLElement
|
||||||
|
}>()
|
||||||
|
const popper = usePopper(props)
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const matchInfo = ref<MatchInfo | undefined>(undefined)
|
||||||
|
const bannerItems = computed(() => matchInfo.value?.banner)
|
||||||
|
const hotItems = computed(() => matchInfo.value?.hot)
|
||||||
|
const previewItems = computed(() => matchInfo.value?.preview)
|
||||||
|
const loadMatchInfo = async () => {
|
||||||
|
matchInfo.value = reactive(
|
||||||
|
await bilibiliApi<MatchInfo>(
|
||||||
|
getJsonWithCredentials('https://api.bilibili.com/x/web-interface/nav/game'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
const toggleSubscribe = async (card: MatchPreviewItem) => {
|
||||||
|
const isSubscribed = card.fav_status !== 0
|
||||||
|
try {
|
||||||
|
await postTextWithCredentials(
|
||||||
|
isSubscribed
|
||||||
|
? 'https://api.bilibili.com/x/esports/fav/del'
|
||||||
|
: 'https://api.bilibili.com/x/esports/fav/add',
|
||||||
|
new URLSearchParams({
|
||||||
|
cid: card.cid.toString(),
|
||||||
|
csrf: getCsrf(),
|
||||||
|
}).toString(),
|
||||||
|
)
|
||||||
|
if (isSubscribed) {
|
||||||
|
card.fav_status = 0
|
||||||
|
} else {
|
||||||
|
card.fav_status = 1
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
popupShow() {
|
||||||
|
popper.popupShow()
|
||||||
|
loadMatchInfo()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="custom-navbar-match-popup">
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<VLoading />
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading" class="custom-navbar-match-panel">
|
||||||
|
<div class="custom-navbar-match-left-panel">
|
||||||
|
<a
|
||||||
|
v-for="card of bannerItems"
|
||||||
|
:key="card.jump_url"
|
||||||
|
class="banner-match-card"
|
||||||
|
:href="card.jump_url"
|
||||||
|
>
|
||||||
|
<img :src="card.cover" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="custom-navbar-match-panel-separator"></div>
|
||||||
|
<div class="custom-navbar-match-right-panel">
|
||||||
|
<div class="hot-match-list">
|
||||||
|
<div class="hot-match-list-title">热门赛事</div>
|
||||||
|
<a
|
||||||
|
v-for="card of hotItems"
|
||||||
|
:key="card.jump_url"
|
||||||
|
:href="card.jump_url"
|
||||||
|
:title="card.name"
|
||||||
|
class="hot-match-card"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="hot-match-card-status"
|
||||||
|
:class="{ 'in-progress': card.status === MatchHotItemStatus.InProgress }"
|
||||||
|
>
|
||||||
|
<template v-if="card.status === MatchHotItemStatus.NotStarted">未开始</template>
|
||||||
|
<template v-if="card.status === MatchHotItemStatus.InProgress">进行中</template>
|
||||||
|
<template v-if="card.status === MatchHotItemStatus.Finished">已结束</template>
|
||||||
|
</div>
|
||||||
|
<div class="hot-match-card-title">
|
||||||
|
{{ card.name }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="preview-match-list">
|
||||||
|
<div class="preview-match-list-title">精彩预告</div>
|
||||||
|
<a
|
||||||
|
v-for="card of previewItems"
|
||||||
|
:key="card.jump_url"
|
||||||
|
:href="card.jump_url"
|
||||||
|
:title="card.name"
|
||||||
|
class="preview-match-card"
|
||||||
|
>
|
||||||
|
<!-- spell-checker: disable-next-line -->
|
||||||
|
<div class="preview-match-date">{{ card.stime }}</div>
|
||||||
|
<div class="preview-match-title">{{ card.name }}</div>
|
||||||
|
<VButton
|
||||||
|
class="preview-match-subscribe"
|
||||||
|
:type="card.fav_status === 0 ? 'primary' : 'light'"
|
||||||
|
@click.prevent="toggleSubscribe(card)"
|
||||||
|
>
|
||||||
|
{{ card.fav_status === 0 ? '订阅' : '已订阅' }}
|
||||||
|
</VButton>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
.custom-navbar-match-popup {
|
||||||
|
width: 650px;
|
||||||
|
font-size: 12px;
|
||||||
|
$gap: 12px;
|
||||||
|
@include v-stretch();
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
@include v-center();
|
||||||
|
justify-content: center;
|
||||||
|
height: 240px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar-match-panel {
|
||||||
|
@include h-stretch($gap);
|
||||||
|
padding: $gap;
|
||||||
|
.custom-navbar-match-left-panel {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 45%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: $gap;
|
||||||
|
.banner-match-card {
|
||||||
|
overflow: hidden;
|
||||||
|
@include round-corner(6px);
|
||||||
|
&:first-child {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #8882;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.custom-navbar-match-panel-separator {
|
||||||
|
width: 1px;
|
||||||
|
background-color: #8882;
|
||||||
|
}
|
||||||
|
.custom-navbar-match-right-panel {
|
||||||
|
@include v-stretch($gap);
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 4px;
|
||||||
|
|
||||||
|
.hot-match-list-title,
|
||||||
|
.preview-match-list-title {
|
||||||
|
font-size: large;
|
||||||
|
@include semi-bold();
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.hot-match-list,
|
||||||
|
.preview-match-list {
|
||||||
|
@include v-stretch();
|
||||||
|
counter-reset: match-item;
|
||||||
|
}
|
||||||
|
.hot-match-card {
|
||||||
|
@include h-center(6px);
|
||||||
|
padding: 5px 0;
|
||||||
|
&-status {
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: #8882;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
&.in-progress {
|
||||||
|
background-color: var(--theme-color);
|
||||||
|
color: var(--foreground-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
@include single-line();
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
counter-increment: match-item;
|
||||||
|
content: counter(match-item);
|
||||||
|
@include semi-bold();
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
&:hover .hot-match-card-title {
|
||||||
|
color: var(--theme-color) !important;
|
||||||
|
}
|
||||||
|
&:nth-child(2)::before {
|
||||||
|
color: #e64747;
|
||||||
|
}
|
||||||
|
&:nth-child(3)::before {
|
||||||
|
color: #ff6e42;
|
||||||
|
}
|
||||||
|
&:nth-child(4)::before {
|
||||||
|
color: #ff9900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.preview-match-card {
|
||||||
|
@include h-center(4px);
|
||||||
|
padding: 5px 0;
|
||||||
|
.preview-match-date {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.preview-match-title {
|
||||||
|
flex: 1 0 0;
|
||||||
|
@include single-line();
|
||||||
|
}
|
||||||
|
.preview-match-subscribe.be-button {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
&:hover .preview-match-title {
|
||||||
|
color: var(--theme-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
registry/lib/components/style/custom-navbar/match/match.ts
Normal file
14
registry/lib/components/style/custom-navbar/match/match.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CustomNavbarItemInit } from '../custom-navbar-item'
|
||||||
|
|
||||||
|
export const match: CustomNavbarItemInit = {
|
||||||
|
name: 'match',
|
||||||
|
displayName: '赛事',
|
||||||
|
content: '赛事',
|
||||||
|
|
||||||
|
touch: true,
|
||||||
|
href: 'https://www.bilibili.com/v/game/match/',
|
||||||
|
|
||||||
|
boundingWidth: 650,
|
||||||
|
noPopupPadding: true,
|
||||||
|
popupContent: () => import('./MatchPopup.vue').then(m => m.default),
|
||||||
|
}
|
||||||
37
registry/lib/components/style/custom-navbar/match/types.ts
Normal file
37
registry/lib/components/style/custom-navbar/match/types.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export interface MatchInfo {
|
||||||
|
hot: MatchHotItem[]
|
||||||
|
banner: MatchBannerItem[]
|
||||||
|
preview: MatchPreviewItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MatchHotItemStatus {
|
||||||
|
NotStarted = 1,
|
||||||
|
InProgress,
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchHotItem {
|
||||||
|
index: number
|
||||||
|
cid: number
|
||||||
|
desc: string
|
||||||
|
status: MatchHotItemStatus
|
||||||
|
name: string
|
||||||
|
jump_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchBannerItem {
|
||||||
|
index: number
|
||||||
|
cover: string
|
||||||
|
jump_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchPreviewItem {
|
||||||
|
index: number
|
||||||
|
cid: number
|
||||||
|
name: string
|
||||||
|
desc: string
|
||||||
|
fav_status: number
|
||||||
|
jump_url: string
|
||||||
|
// spell-checker: disable-next-line
|
||||||
|
stime: string
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { onMounted } from 'vue'
|
||||||
import { CustomNavbarItem } from './custom-navbar-item'
|
import { CustomNavbarItem } from './custom-navbar-item'
|
||||||
|
|
||||||
export const popperMixin = Vue.extend({
|
export const popperMixin = Vue.extend({
|
||||||
@ -25,3 +26,23 @@ export const popperMixin = Vue.extend({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export interface UsePopperProps {
|
||||||
|
item: CustomNavbarItem
|
||||||
|
container: HTMLElement
|
||||||
|
}
|
||||||
|
export const usePopper = (props: UsePopperProps) => {
|
||||||
|
onMounted(() => {
|
||||||
|
const navBarItem = props.item
|
||||||
|
const containerElement = props.container
|
||||||
|
if (containerElement) {
|
||||||
|
navBarItem?.usePopper(containerElement, containerElement.children[0] as HTMLElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
popupShow() {
|
||||||
|
const navBarItem = props.item
|
||||||
|
navBarItem?.popper?.update()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { getComponentSettings } from '@/core/settings'
|
||||||
|
import type { CustomNavbarOptions } from '.'
|
||||||
|
|
||||||
|
export const migrateOrders = () => {
|
||||||
|
const migrations = [
|
||||||
|
['gamesIframe', 'games'],
|
||||||
|
['livesIframe', 'lives'],
|
||||||
|
['mangaIframe', 'manga'],
|
||||||
|
]
|
||||||
|
|
||||||
|
const { options } = getComponentSettings<CustomNavbarOptions>('customNavbar')
|
||||||
|
migrations.forEach(([from, to]) => {
|
||||||
|
const oldOrder = options.order[from]
|
||||||
|
if (oldOrder !== undefined) {
|
||||||
|
options.order[to] = oldOrder
|
||||||
|
delete options.order[from]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -32,13 +32,13 @@ export const shop = getSimpleLinkItem({
|
|||||||
displayName: '会员购',
|
displayName: '会员购',
|
||||||
href: 'https://show.bilibili.com',
|
href: 'https://show.bilibili.com',
|
||||||
})
|
})
|
||||||
export const match = getSimpleLinkItem({
|
|
||||||
name: 'match',
|
|
||||||
displayName: '赛事',
|
|
||||||
href: 'https://www.bilibili.com/v/game/match/',
|
|
||||||
})
|
|
||||||
export const creations = getSimpleLinkItem({
|
export const creations = getSimpleLinkItem({
|
||||||
name: 'creations',
|
name: 'creations',
|
||||||
displayName: '创作中心',
|
displayName: '创作中心',
|
||||||
href: 'https://member.bilibili.com/platform/home',
|
href: 'https://member.bilibili.com/platform/home',
|
||||||
})
|
})
|
||||||
|
export const lives = getSimpleLinkItem({
|
||||||
|
name: 'lives',
|
||||||
|
displayName: '直播',
|
||||||
|
href: 'https://live.bilibili.com',
|
||||||
|
})
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
.search-panel {
|
.search-panel, .search-pannel {
|
||||||
|
|
||||||
> .trending {
|
> .trending {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:has(.history)) {
|
&:not(:has(.history)) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|||||||
@ -9,6 +9,26 @@ const resetPlaceholder = (input: HTMLInputElement) => {
|
|||||||
input.title = DEFAULT_PLACEHOLDER
|
input.title = DEFAULT_PLACEHOLDER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInLiveRoomPage = () => {
|
||||||
|
return window.location.host === 'live.bilibili.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchInputClassName = () => {
|
||||||
|
return isInLiveRoomPage() ? 'input.nav-search-content' : 'input.nav-search-input'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSearchInput = (target: Node) => {
|
||||||
|
if (!(target instanceof HTMLInputElement)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSearch = ['nav-search-content', 'nav-search-input'].some(n =>
|
||||||
|
target.classList.contains(n),
|
||||||
|
)
|
||||||
|
|
||||||
|
return target.placeholder !== DEFAULT_PLACEHOLDER && isSearch
|
||||||
|
}
|
||||||
|
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
name: 'hideTrendingSearch',
|
name: 'hideTrendingSearch',
|
||||||
displayName: '隐藏热搜',
|
displayName: '隐藏热搜',
|
||||||
@ -20,7 +40,8 @@ export const component = defineComponentMetadata({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
entry: async () => {
|
entry: async () => {
|
||||||
const input: HTMLInputElement = await select('input.nav-search-input', {
|
const clsName = getSearchInputClassName()
|
||||||
|
const input: HTMLInputElement = await select(clsName, {
|
||||||
queryInterval: 500,
|
queryInterval: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -32,12 +53,8 @@ export const component = defineComponentMetadata({
|
|||||||
// Fallback to observer
|
// Fallback to observer
|
||||||
allMutations(records => {
|
allMutations(records => {
|
||||||
records.forEach(record => {
|
records.forEach(record => {
|
||||||
if (
|
if (isSearchInput(record.target)) {
|
||||||
record.target instanceof HTMLInputElement &&
|
resetPlaceholder(record.target as HTMLInputElement)
|
||||||
record.target.classList.contains('nav-search-input') &&
|
|
||||||
record.target.placeholder !== DEFAULT_PLACEHOLDER
|
|
||||||
) {
|
|
||||||
resetPlaceholder(record.target)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
这个组件会将视频简介中的普通网址转换为可点击的链接,并将<del>被 Bilibili 抛弃</del>已失效的 `acg.tv` 跳转链接修复为 `nicovideo.jp` 链接。
|
||||||
|
|
||||||
|
例如: `https://acg.tv/sm37507315` → `https://www.nicovideo.jp/watch/sm37507315`
|
||||||
|
|
||||||
|
<i>本组件不会保证目标链接的安全性,因此在点击前请自行验证其是否可信</i>
|
||||||
151
registry/lib/components/utils/active-video-links/index.ts
Normal file
151
registry/lib/components/utils/active-video-links/index.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
|
import { videoUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
|
const console = useScopedConsole('activeVideoLinks')
|
||||||
|
let observer: MutationObserver | null = null
|
||||||
|
const webRegex = /(?<!(\>|'|"|\/))(http:\/\/|https:\/\/|www\.)[^(\s,;())]+/g
|
||||||
|
|
||||||
|
const processAcgLinks = () => {
|
||||||
|
const links = document.querySelectorAll('a[href*="acg.tv"][href*="sm"]')
|
||||||
|
links.forEach(link => {
|
||||||
|
const originalHref = link.getAttribute('href')
|
||||||
|
if (originalHref) {
|
||||||
|
const newHref = originalHref.replace('acg.tv', 'nicovideo.jp/watch')
|
||||||
|
link.setAttribute('href', newHref)
|
||||||
|
console.log(`Niconico Fix: ${originalHref} → ${newHref}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const processDescLinks = () => {
|
||||||
|
const descContainer = document.querySelector('.desc-info-text')
|
||||||
|
if (!descContainer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const walker = document.createTreeWalker(descContainer, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode: node => {
|
||||||
|
if (node.parentElement?.closest('a')) {
|
||||||
|
return NodeFilter.FILTER_REJECT
|
||||||
|
}
|
||||||
|
return webRegex.test(node.textContent || '')
|
||||||
|
? NodeFilter.FILTER_ACCEPT
|
||||||
|
: NodeFilter.FILTER_REJECT
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const textNodes: Text[] = []
|
||||||
|
let currentNode: Node | null = walker.nextNode()
|
||||||
|
while (currentNode !== null) {
|
||||||
|
textNodes.push(currentNode as Text)
|
||||||
|
currentNode = walker.nextNode()
|
||||||
|
}
|
||||||
|
textNodes.forEach(textNode => {
|
||||||
|
const frag = document.createDocumentFragment()
|
||||||
|
const text = textNode.textContent || ''
|
||||||
|
let lastIndex = 0
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
webRegex.lastIndex = 0
|
||||||
|
match = webRegex.exec(text)
|
||||||
|
while (match !== null) {
|
||||||
|
const start = match.index
|
||||||
|
if (lastIndex < start) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(lastIndex, start)))
|
||||||
|
}
|
||||||
|
const urlText = match[0]
|
||||||
|
const linkHref = urlText.replace(/^https?:\/\//, '//').replace(/^www\./, '//')
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = linkHref
|
||||||
|
a.target = '_blank'
|
||||||
|
a.textContent = urlText
|
||||||
|
frag.appendChild(a)
|
||||||
|
lastIndex = start + urlText.length
|
||||||
|
match = webRegex.exec(text)
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(lastIndex)))
|
||||||
|
}
|
||||||
|
textNode.parentNode?.replaceChild(frag, textNode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeNicoDescLinks = () => {
|
||||||
|
const descContainer = document.querySelector('.desc-info-text')
|
||||||
|
if (!descContainer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const anchors = Array.from(descContainer.querySelectorAll('a'))
|
||||||
|
for (let i = 0; i < anchors.length - 1; i++) {
|
||||||
|
const a1 = anchors[i]
|
||||||
|
const a2 = anchors[i + 1]
|
||||||
|
const href1 = a1.getAttribute('href') || ''
|
||||||
|
const text2 = a2.textContent?.trim() || ''
|
||||||
|
const href2 = a2.getAttribute('href') || ''
|
||||||
|
// first link is base watch URL, second has sm ID text
|
||||||
|
if (
|
||||||
|
/^(https?:)?\/\/(www\.)?nicovideo\.jp\/watch\/?$/.test(href1) &&
|
||||||
|
/^sm\d+$/.test(text2) &&
|
||||||
|
href2.includes(`/watch/${text2}`)
|
||||||
|
) {
|
||||||
|
const newHref = href2.replace(/^(https?:)?/, '//')
|
||||||
|
const newA = document.createElement('a')
|
||||||
|
newA.href = newHref
|
||||||
|
newA.target = '_blank'
|
||||||
|
newA.textContent = text2
|
||||||
|
a1.replaceWith(newA)
|
||||||
|
a2.remove()
|
||||||
|
console.log(`Merged Nico link: ${text2}`)
|
||||||
|
// skip the next element
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processLinks = () => {
|
||||||
|
try {
|
||||||
|
processAcgLinks()
|
||||||
|
processDescLinks()
|
||||||
|
normalizeNicoDescLinks()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理链接时遇到 Error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupObservers = () => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
observer = new MutationObserver(processLinks)
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('已设置 Observer')
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = () => {
|
||||||
|
console.log('视频链接增强已启用')
|
||||||
|
processLinks()
|
||||||
|
setupObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'activeVideoLinks',
|
||||||
|
displayName: '视频链接增强',
|
||||||
|
tags: [componentsTags.utils],
|
||||||
|
entry,
|
||||||
|
reload: entry,
|
||||||
|
unload: () => {
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect()
|
||||||
|
observer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
urlInclude: videoUrls,
|
||||||
|
author: {
|
||||||
|
name: 'Alan Ye',
|
||||||
|
link: 'https://github.com/at-wr',
|
||||||
|
},
|
||||||
|
})
|
||||||
64
registry/lib/components/utils/download-emoticons/Widget.vue
Normal file
64
registry/lib/components/utils/download-emoticons/Widget.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<DefaultWidget
|
||||||
|
:disabled="downloading"
|
||||||
|
name="下载up主表情包"
|
||||||
|
icon="mdi-emoticon-outline"
|
||||||
|
@click="download()"
|
||||||
|
></DefaultWidget>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { DefaultWidget } from '@/ui'
|
||||||
|
import { logError } from '@/core/utils/log'
|
||||||
|
import { Emoticons } from './emoticons'
|
||||||
|
import { DownloadPackage } from '@/core/download'
|
||||||
|
import { Toast } from '@/core/toast'
|
||||||
|
|
||||||
|
const downloader = new Emoticons()
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
DefaultWidget,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
downloading: false,
|
||||||
|
midRegex: /mid=(\d+)/,
|
||||||
|
roomIdRegex: /^https:\/\/live\.bilibili\.com\/(\d+)(?:\?.*)?$/,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async download() {
|
||||||
|
this.downloading = true
|
||||||
|
try {
|
||||||
|
const url = window.location.href
|
||||||
|
if (url.startsWith('https://live.bilibili.com/')) {
|
||||||
|
const rid = this.roomIdRegex.exec(url)[1]
|
||||||
|
const emoticonsArray = await downloader.getEmoticonsArray(rid)
|
||||||
|
if (emoticonsArray.length === 0) {
|
||||||
|
Toast.info('该up主没有专属表情包', '表情包下载')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const downloadPromises = await downloader.downloadEmoticons(emoticonsArray)
|
||||||
|
const results = await downloader.batchDownload(downloadPromises)
|
||||||
|
const dpackage = new DownloadPackage()
|
||||||
|
dpackage.noEscape = true
|
||||||
|
for (const result of results) {
|
||||||
|
if (result !== null) {
|
||||||
|
for (const emoticon of result.emoticons) {
|
||||||
|
dpackage.add(`${result.pkg_name}/${emoticon.emoji}.png`, emoticon.blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await dpackage.emit(`emoticons-${rid}.zip`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Toast.error('下载失败详情查看控制台', '表情包下载')
|
||||||
|
logError(error)
|
||||||
|
} finally {
|
||||||
|
this.downloading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
106
registry/lib/components/utils/download-emoticons/emoticons.ts
Normal file
106
registry/lib/components/utils/download-emoticons/emoticons.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { monkey } from '@/core/ajax'
|
||||||
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
|
|
||||||
|
const logger = useScopedConsole('表情包下载')
|
||||||
|
|
||||||
|
interface DownloadPromise {
|
||||||
|
pkg_name: string // 表情包名称
|
||||||
|
emoticons: { emoji: string; blob: Promise<Blob> }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Emoticons {
|
||||||
|
private async getEmoticonsByRoomId(roomId: string) {
|
||||||
|
const response = await this.apiRequest(
|
||||||
|
`https://api.live.bilibili.com/xlive/web-ucenter/v2/emoticon/GetEmoticons?platform=pc&room_id=${roomId}`,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
async downloadEmoticons(emoticonsArray) {
|
||||||
|
const downloadPromises: DownloadPromise[] = emoticonsArray.map(obj => {
|
||||||
|
const emoticons = obj.emoticons.map(item => {
|
||||||
|
try {
|
||||||
|
const blob = this.getImageBlob(item.url)
|
||||||
|
return {
|
||||||
|
emoji: item.emoji,
|
||||||
|
blob,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`下载表情 ${item.emoji} 失败:`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
emoticons,
|
||||||
|
pkg_name: obj.pkg_name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return downloadPromises
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmoticonsArray(roomId: string): Promise<Array<any>> {
|
||||||
|
try {
|
||||||
|
let response = await this.getEmoticonsByRoomId(roomId)
|
||||||
|
if (response.data.data.length === 0) {
|
||||||
|
logger.log('获取表情包失败,尝试获取真实roomId')
|
||||||
|
const realRoomId = await this.getRealRoomId(roomId)
|
||||||
|
response = await this.getEmoticonsByRoomId(realRoomId)
|
||||||
|
}
|
||||||
|
return response.data.data.slice(2)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('获取表情包失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchDownload(tasks: DownloadPromise[]) {
|
||||||
|
const results: any[] = []
|
||||||
|
for (const task of tasks) {
|
||||||
|
const promise = (async () => {
|
||||||
|
const blobs: Blob[] = []
|
||||||
|
const blobPromises = task.emoticons.map(e => e.blob)
|
||||||
|
for (const blobPromise of blobPromises) {
|
||||||
|
const blob = await blobPromise
|
||||||
|
blobs.push(blob)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pkg_name: task.pkg_name,
|
||||||
|
emoticons: task.emoticons.map((e, i) => ({ emoji: e.emoji, blob: blobs[i] })),
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
results.push(promise)
|
||||||
|
}
|
||||||
|
return Promise.all(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有的直播间id是超短id,需要获取到原始的roomid才能通过api获取到表情包
|
||||||
|
async getRealRoomId(rid: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await this.apiRequest(
|
||||||
|
`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${rid}`,
|
||||||
|
)
|
||||||
|
if (Object.keys(response.data).length === 0) {
|
||||||
|
throw new Error('获取直播间id失败')
|
||||||
|
}
|
||||||
|
return response.data.room_id
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('获取直播间id失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiRequest = (url: string): Promise<any> => {
|
||||||
|
const details: MonkeyXhrBasicDetails = {
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'json',
|
||||||
|
}
|
||||||
|
return monkey(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageBlob = async (url: string) => {
|
||||||
|
const details: MonkeyXhrBasicDetails = {
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
}
|
||||||
|
return monkey<Blob>(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
## 下载up主专属表情包
|
||||||
|
|
||||||
|
支持下载up主专属表情包,处于任意直播间页面时,下载按钮会在`功能`面板显示,以压缩包形式保存。
|
||||||
16
registry/lib/components/utils/download-emoticons/index.ts
Normal file
16
registry/lib/components/utils/download-emoticons/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'downloadEmoticons',
|
||||||
|
displayName: '下载表情',
|
||||||
|
tags: [componentsTags.utils, componentsTags.live],
|
||||||
|
entry: none,
|
||||||
|
author: {
|
||||||
|
name: 'Pencilqaq',
|
||||||
|
link: 'https://github.com/pencilqaq',
|
||||||
|
},
|
||||||
|
widget: {
|
||||||
|
component: () => import('./Widget.vue').then(m => m.default),
|
||||||
|
},
|
||||||
|
urlInclude: [/^https:\/\/live\.bilibili\.com\/(\d+)/],
|
||||||
|
})
|
||||||
@ -0,0 +1 @@
|
|||||||
|
重定向**稍后再看页面**到普通视频页面,和`稍后再看重定向`组件的区别在于,该组件是在进入稍后再看的视频页面后进行重定向,而非直接替换链接到普通视频的链接,用于补充`稍后再看重定向`组件的功能。
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { ComponentEntry } from '@/components/types'
|
||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
|
||||||
|
const getBvidFromWatcherLaterVideoPage = () => {
|
||||||
|
const { pathname, search } = window.location
|
||||||
|
const oidAndBvid = search.match(/\?oid=(\d+)&bvid=(\w+)/) ?? []
|
||||||
|
|
||||||
|
if (pathname === '/list/watchlater' && oidAndBvid.length === 3) {
|
||||||
|
return oidAndBvid[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry: ComponentEntry = async () => {
|
||||||
|
// 判断是否是稍后再看链接格式的视频页面,如果是则重定向
|
||||||
|
const bvid = getBvidFromWatcherLaterVideoPage()
|
||||||
|
|
||||||
|
if (bvid) {
|
||||||
|
location.href = `https://www.bilibili.com/video/${bvid}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'watchlaterPageRedirect',
|
||||||
|
displayName: '稍后再看页面-重定向',
|
||||||
|
entry,
|
||||||
|
author: {
|
||||||
|
name: 'magicFeirl',
|
||||||
|
link: 'https://github.com/magicFeirl',
|
||||||
|
},
|
||||||
|
urlInclude: ['https://www.bilibili.com/list/watchlater'],
|
||||||
|
tags: [componentsTags.utils, componentsTags.video],
|
||||||
|
})
|
||||||
@ -194,32 +194,45 @@ export const responsiveGetPages = <T = any>(config: {
|
|||||||
responsivePromise = new Promise(resolveResponsive => {
|
responsivePromise = new Promise(resolveResponsive => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const { api, getList, getTotal } = config
|
const { api, getList, getTotal } = config
|
||||||
let page = 1
|
|
||||||
let total = Infinity
|
let total = Infinity
|
||||||
const result = []
|
const result: T[] = []
|
||||||
while (result.length < total) {
|
|
||||||
const json = await api(page)
|
const fetchPage = async (p: number): Promise<T[]> => {
|
||||||
|
const json = await api(p)
|
||||||
if (json.code !== 0) {
|
if (json.code !== 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`api failed in ajax.getPages. message = ${json.message}, page = ${page}, total = ${total}, api = `,
|
`api failed in ajax.getPages. message = ${json.message}, page = ${p}, total = ${total}, api = `,
|
||||||
api,
|
api,
|
||||||
)
|
)
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
const list = getList(json)
|
return getList(json)
|
||||||
result.push(...list)
|
|
||||||
if (page === 1) {
|
|
||||||
resolveResponsive(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.length === 0) {
|
// 请求第一次获得 total
|
||||||
|
const firstReq = await api(1)
|
||||||
|
result.push(...getList(firstReq))
|
||||||
|
|
||||||
|
total = getTotal(firstReq)
|
||||||
|
const pageSize = getList(firstReq).length || 1 // 防止为 0
|
||||||
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
resolveResponsive(result) // 第一页
|
||||||
|
if (totalPages === 1) {
|
||||||
resolveTotal(result)
|
resolveTotal(result)
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
page++
|
// 收集 Promise,每 5 个为一个 batch 运行
|
||||||
if (total === Infinity) {
|
const batchSize = 5
|
||||||
total = getTotal(json)
|
let batch: Promise<T[]>[] = []
|
||||||
|
for (let i = 2; i <= totalPages; i++) {
|
||||||
|
batch.push(fetchPage(i))
|
||||||
|
if (batch.length === batchSize || i === totalPages) {
|
||||||
|
const lists = await Promise.all(batch)
|
||||||
|
result.push(...lists.flat())
|
||||||
|
batch = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 等所有并发完成
|
||||||
resolveTotal(result)
|
resolveTotal(result)
|
||||||
})()
|
})()
|
||||||
})
|
})
|
||||||
@ -242,6 +255,7 @@ export const getPages = async <T = any>(config: {
|
|||||||
const result = await total
|
const result = await total
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/** bilibili API 标准响应 */
|
/** bilibili API 标准响应 */
|
||||||
export interface BilibiliApiResponse {
|
export interface BilibiliApiResponse {
|
||||||
code: number
|
code: number
|
||||||
@ -251,6 +265,7 @@ export interface BilibiliApiResponse {
|
|||||||
data: any
|
data: any
|
||||||
result?: any
|
result?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 进行 bilibili API 标准响应处理
|
* 进行 bilibili API 标准响应处理
|
||||||
* @param apiPromise 运行中的 API Promise
|
* @param apiPromise 运行中的 API Promise
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user