mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-07 02:46:55 +08:00
add live
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
|
"fast-xml-parser": "^4.3.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"vue-turnstile": "^1.0.0",
|
"vue-turnstile": "^1.0.0",
|
||||||
"vue3-aplayer": "^1.7.3",
|
"vue3-aplayer": "^1.7.3",
|
||||||
"vue3-marquee": "^4.1.0",
|
"vue3-marquee": "^4.1.0",
|
||||||
|
"vueuc": "^0.4.51",
|
||||||
"vuex": "^4.0.0",
|
"vuex": "^4.0.0",
|
||||||
"worker-timers": "^7.0.78"
|
"worker-timers": "^7.0.78"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -410,3 +410,30 @@ export interface ResponseQueueModel {
|
|||||||
finishAt?: number | null
|
finishAt?: number | null
|
||||||
isInLocal?: boolean
|
isInLocal?: boolean
|
||||||
}
|
}
|
||||||
|
export interface ResponseLiveInfoModel {
|
||||||
|
liveId: string
|
||||||
|
isFinish: boolean
|
||||||
|
parentArea: string
|
||||||
|
area: string
|
||||||
|
coverUrl: string
|
||||||
|
danmakusCount: number
|
||||||
|
startAt: number
|
||||||
|
stopAt: number | null
|
||||||
|
title: string
|
||||||
|
totalIncome: number
|
||||||
|
totalIncomeWithGuard: number
|
||||||
|
likeCount: number
|
||||||
|
paymentCount: number
|
||||||
|
interactionCount: number
|
||||||
|
}
|
||||||
|
export interface DanmakuModel {
|
||||||
|
id: string
|
||||||
|
uId: number
|
||||||
|
uName: string
|
||||||
|
type: EventDataTypes // Assuming EventDataTypes is an enum or type available in your TypeScript codebase
|
||||||
|
time: number
|
||||||
|
msg: string | null
|
||||||
|
price: number | null
|
||||||
|
isEmoji: boolean
|
||||||
|
num: number
|
||||||
|
}
|
||||||
|
|||||||
549
src/components/DanmakuContainer.vue
Normal file
549
src/components/DanmakuContainer.vue
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, h, computed, nextTick, watch, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
NSkeleton,
|
||||||
|
NCard,
|
||||||
|
NDivider,
|
||||||
|
NCheckbox,
|
||||||
|
NCheckboxGroup,
|
||||||
|
NModal,
|
||||||
|
NButton,
|
||||||
|
NPopover,
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NSpace,
|
||||||
|
NSwitch,
|
||||||
|
NInputNumber,
|
||||||
|
NCollapseTransition,
|
||||||
|
NTag,
|
||||||
|
NSpin,
|
||||||
|
NRadioGroup,
|
||||||
|
NList,
|
||||||
|
NListItem,
|
||||||
|
NAvatar,
|
||||||
|
NTooltip,
|
||||||
|
NRadioButton,
|
||||||
|
NCollapse,
|
||||||
|
NCollapseItem,
|
||||||
|
} from 'naive-ui'
|
||||||
|
import router from '@/router'
|
||||||
|
import { Search24Filled, Money20Regular, Wrench24Filled, Money24Regular, Info12Filled, Home24Filled } from '@vicons/fluent'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { useLocalStorage, useWindowSize, useDebounceFn } from '@vueuse/core'
|
||||||
|
import { ResponseLiveInfoModel, DanmakuModel, EventDataTypes } from '@/api/api-models'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
import { useAccount } from '@/api/account'
|
||||||
|
import LiveInfoContainer from './LiveInfoContainer.vue'
|
||||||
|
import { GetString } from '@/data/DanmakuExport'
|
||||||
|
import SimpleVirtualList from './SimpleVirtualList.vue'
|
||||||
|
import DanmakuItem from '@/components/DanmakuItem.vue'
|
||||||
|
|
||||||
|
enum RankType {
|
||||||
|
Danmaku,
|
||||||
|
Paid,
|
||||||
|
}
|
||||||
|
interface RankInfo {
|
||||||
|
uId: number
|
||||||
|
uName: string
|
||||||
|
Paid: number
|
||||||
|
Danmakus: number
|
||||||
|
Index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountInfo = useAccount()
|
||||||
|
|
||||||
|
const debouncedFn = useDebounceFn(() => {
|
||||||
|
UpdateDanmakus()
|
||||||
|
}, 750)
|
||||||
|
let isLoading = ref(false)
|
||||||
|
let showModal = ref(false)
|
||||||
|
let showExportModal = ref(false)
|
||||||
|
let userDanmakus = ref<DanmakuModel[] | undefined>()
|
||||||
|
let hideAvatar = useLocalStorage('Setting.HideAvatar', false)
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentLive: ResponseLiveInfoModel
|
||||||
|
currentDanmakus: DanmakuModel[]
|
||||||
|
defaultFilterSelected?: EventDataTypes[]
|
||||||
|
height?: number
|
||||||
|
itemHeight?: number
|
||||||
|
showLiver?: boolean
|
||||||
|
showLiveInfo?: boolean
|
||||||
|
showName?: boolean
|
||||||
|
//to?: ClickNameTo;
|
||||||
|
isInModal?: boolean
|
||||||
|
showRank?: boolean
|
||||||
|
showBorder?: boolean
|
||||||
|
showTools?: boolean
|
||||||
|
showStatistic?: boolean
|
||||||
|
animeNum?: boolean
|
||||||
|
bordered?: boolean
|
||||||
|
toolsVisiable?: boolean
|
||||||
|
itemRange?: number
|
||||||
|
to: 'userDanmakus' | 'space'
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentDanmakus,
|
||||||
|
currentLive,
|
||||||
|
defaultFilterSelected = [EventDataTypes.Gift, EventDataTypes.Guard, EventDataTypes.Message, EventDataTypes.SC],
|
||||||
|
height = 1000,
|
||||||
|
itemHeight = 30,
|
||||||
|
showLiveInfo = true,
|
||||||
|
showLiver = false,
|
||||||
|
//to = ClickNameTo.UserHistory,
|
||||||
|
isInModal = false,
|
||||||
|
showName = true,
|
||||||
|
showBorder = true,
|
||||||
|
showTools = true,
|
||||||
|
showStatistic = true,
|
||||||
|
animeNum = true,
|
||||||
|
showRank = false,
|
||||||
|
bordered = true,
|
||||||
|
toolsVisiable = true,
|
||||||
|
itemRange = 30,
|
||||||
|
to = 'space',
|
||||||
|
} = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'onClickName', uId: number): boolean
|
||||||
|
}>()
|
||||||
|
defineExpose({
|
||||||
|
InsertDanmakus,
|
||||||
|
})
|
||||||
|
let danmakus = ref<DanmakuModel[]>([])
|
||||||
|
watch(
|
||||||
|
() => showTools,
|
||||||
|
(newV, oldV) => {
|
||||||
|
innerShowTools.value = newV
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const danmakuRef = computed(() => {
|
||||||
|
//不知道为啥不能直接watch
|
||||||
|
return currentDanmakus
|
||||||
|
})
|
||||||
|
watch(danmakuRef, (newValue) => {
|
||||||
|
danmakus.value = GetFilteredDanmakus(newValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
let keyword = ref('')
|
||||||
|
let enableRegex = ref(false)
|
||||||
|
let deselect = ref(false)
|
||||||
|
let price = ref<number | undefined>()
|
||||||
|
let filterSelected = ref(defaultFilterSelected)
|
||||||
|
let innerShowTools = ref(showTools)
|
||||||
|
let modalShowTools = ref(false)
|
||||||
|
let orderDecreasing = ref(false)
|
||||||
|
let orderByPrice = ref(false)
|
||||||
|
let processing = ref(false)
|
||||||
|
let isRanking = ref(false)
|
||||||
|
let rankInfo = ref<RankInfo[]>([])
|
||||||
|
let currentRankInfo = ref<RankInfo[]>([])
|
||||||
|
let rankType = ref(RankType.Danmaku)
|
||||||
|
let hideEmoji = ref(false)
|
||||||
|
|
||||||
|
let exportType = ref<'json' | 'xml' | 'csv'>('json')
|
||||||
|
let onlyExportFilteredDanmakus = ref(false)
|
||||||
|
let isExporting = ref(false)
|
||||||
|
|
||||||
|
function OnNameClick(uId: number) {
|
||||||
|
if (isInModal) {
|
||||||
|
emit('onClickName', uId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (to) {
|
||||||
|
case 'userDanmakus': {
|
||||||
|
userDanmakus.value = currentDanmakus.filter((d) => d.uId == uId)
|
||||||
|
showModal.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'space': {
|
||||||
|
showModal.value = false
|
||||||
|
nextTick(() => {
|
||||||
|
window.open('https://space.bilibili.com/' + uId, '_blank')
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ToUserSpace(uId: number) {
|
||||||
|
showModal.value = false
|
||||||
|
nextTick(() => {
|
||||||
|
router.push({
|
||||||
|
name: 'user',
|
||||||
|
params: {
|
||||||
|
uid: uId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function ChangePrice(p: number) {
|
||||||
|
if (p == price.value) price.value = undefined
|
||||||
|
else {
|
||||||
|
price.value = p
|
||||||
|
}
|
||||||
|
UpdateDanmakus()
|
||||||
|
}
|
||||||
|
function UpdateDanmakus() {
|
||||||
|
processing.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
canInsert = false
|
||||||
|
danmakus.value = GetFilteredDanmakus()
|
||||||
|
setTimeout(() => {
|
||||||
|
canInsert = true
|
||||||
|
}, 1000) //立马就能的话会莫名key重复
|
||||||
|
processing.value = false
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function OnRank(isRank: boolean) {
|
||||||
|
if (isRank) OnRankDirect(rankType.value, rankInfo.value.length == 0 && danmakus.value.length > 0)
|
||||||
|
}
|
||||||
|
function OnRankDirect(type: RankType, refresh: boolean, orderByDescending = true) {
|
||||||
|
if (refresh) {
|
||||||
|
var rank = {} as {
|
||||||
|
[uId: number]: RankInfo
|
||||||
|
}
|
||||||
|
currentDanmakus.forEach((danmaku) => {
|
||||||
|
if (danmaku.uId in rank) {
|
||||||
|
if (danmaku.type == EventDataTypes.Message) rank[danmaku.uId].Danmakus++
|
||||||
|
rank[danmaku.uId].Paid += danmaku.price ?? 0
|
||||||
|
} else {
|
||||||
|
rank[danmaku.uId] = {
|
||||||
|
uId: danmaku.uId,
|
||||||
|
uName: danmaku.uName,
|
||||||
|
Paid: danmaku.price ?? 0,
|
||||||
|
Danmakus: danmaku.type == EventDataTypes.Message ? 1 : 0,
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
rankInfo.value = new List(Object.entries(rank)).Select(([uId, user]) => user).ToArray()
|
||||||
|
}
|
||||||
|
var ienum = {} as List<RankInfo>
|
||||||
|
switch (rankType.value) {
|
||||||
|
case RankType.Danmaku: {
|
||||||
|
ienum = new List(rankInfo.value).OrderByDescending((user) => user.Danmakus)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case RankType.Paid: {
|
||||||
|
ienum = new List(rankInfo.value).Where((user) => (user?.Paid ?? 0) > 0).OrderByDescending((user) => user.Paid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!orderByDescending) ienum = ienum.Reverse()
|
||||||
|
currentRankInfo.value = ienum.Take(100).ToArray()
|
||||||
|
var index = 1
|
||||||
|
currentRankInfo.value.forEach((info) => {
|
||||||
|
info.Index = index
|
||||||
|
index++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function GetRankIndexColor(index: number) {
|
||||||
|
switch (index) {
|
||||||
|
case 1:
|
||||||
|
return `background:#fbda41;color:rgb(133,133,133);font-size:16px;`
|
||||||
|
case 2:
|
||||||
|
return `background:#c4d7d6;color:rgb(133,133,133);font-size:16px;`
|
||||||
|
case 3:
|
||||||
|
return `background:#f0d695;color:rgb(133,133,133);font-size:16px;`
|
||||||
|
default:
|
||||||
|
return 'background:#afafaf;'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function Export() {
|
||||||
|
isExporting.value = true
|
||||||
|
saveAs(
|
||||||
|
new Blob([GetString(accountInfo.value, currentLive, onlyExportFilteredDanmakus.value ? danmakus.value : currentDanmakus, exportType.value)], { type: 'text/plain;charset=utf-8' }),
|
||||||
|
`${Date.now()}_${currentLive.startAt}_${currentLive.title.replace('_', '-')}_${accountInfo.value?.name}.${exportType.value}`
|
||||||
|
)
|
||||||
|
isExporting.value = false
|
||||||
|
}
|
||||||
|
let canInsert = false
|
||||||
|
function InsertDanmakus(targetDanmakus: DanmakuModel[]) {
|
||||||
|
if (processing.value || !canInsert) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var data = GetFilteredDanmakus(targetDanmakus)
|
||||||
|
if (orderDecreasing.value) {
|
||||||
|
danmakus.value.unshift(...data)
|
||||||
|
currentDanmakus.unshift(...data)
|
||||||
|
} else {
|
||||||
|
danmakus.value.push(...data)
|
||||||
|
currentDanmakus.push(...data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function GetFilteredDanmakus(targetDanmakus?: DanmakuModel[]) {
|
||||||
|
if (!targetDanmakus) targetDanmakus = currentDanmakus
|
||||||
|
var tempDanmakus = targetDanmakus.filter((d) => filterSelected.value.includes(d.type))
|
||||||
|
if (hideEmoji.value) {
|
||||||
|
tempDanmakus = tempDanmakus.filter((d) => d.type != EventDataTypes.Message || !d.isEmoji)
|
||||||
|
}
|
||||||
|
if (orderByPrice.value) {
|
||||||
|
tempDanmakus = tempDanmakus.filter((d) => d.type != EventDataTypes.Message).sort((a, b) => (a.price ?? 0) - (b.price ?? 0))
|
||||||
|
} else {
|
||||||
|
tempDanmakus = tempDanmakus.sort((a, b) => a.time - b.time)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.value && keyword.value != '') {
|
||||||
|
tempDanmakus = tempDanmakus.filter((d) => (deselect.value ? !CheckKeyword(d) : CheckKeyword(d)))
|
||||||
|
}
|
||||||
|
function CheckKeyword(d: DanmakuModel) {
|
||||||
|
if (d.uId.toString() == keyword.value || d.uName == keyword.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return enableRegex.value ? (d.msg?.match(keyword.value) ? true : false) : d.msg?.toLowerCase().includes(keyword.value.toLowerCase()) == true
|
||||||
|
}
|
||||||
|
if (price.value && price.value > 0) {
|
||||||
|
tempDanmakus = tempDanmakus.filter((d) => (d.price ?? 0) >= (price.value ?? 0))
|
||||||
|
}
|
||||||
|
if (orderDecreasing.value) tempDanmakus = tempDanmakus.reverse()
|
||||||
|
var index = 0
|
||||||
|
tempDanmakus.forEach((d) => {
|
||||||
|
d.id = `${d.uId}_${d.time}_${index}`
|
||||||
|
index++
|
||||||
|
})
|
||||||
|
return tempDanmakus
|
||||||
|
}
|
||||||
|
function RoundNumber(num: number) {
|
||||||
|
if (Number.isInteger(num)) {
|
||||||
|
return num
|
||||||
|
} else {
|
||||||
|
return num.toFixed(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
danmakus.value = currentDanmakus
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NSkeleton v-if="isLoading"> </NSkeleton>
|
||||||
|
<NSpin v-else :show="processing">
|
||||||
|
<NModal v-model:show="showModal" @after-leave="userDanmakus = undefined" preset="card" :style="'width: 600px;max-width: 90vw;max-height: 90vh;'" content-style="overflow-y: auto">
|
||||||
|
<template #header>
|
||||||
|
{{ userDanmakus?.[0].uName }}
|
||||||
|
<NPopover>
|
||||||
|
<template #trigger>
|
||||||
|
<NButton text @click="ToUserSpace(userDanmakus?.[0].uId ?? 0)">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Search24Filled" />
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
查询发言记录
|
||||||
|
</NPopover>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<NSwitch v-model:value="modalShowTools" size="small">
|
||||||
|
<template #checked> 显示 </template>
|
||||||
|
<template #unchecked> 隐藏 </template>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Wrench24Filled" />
|
||||||
|
</template>
|
||||||
|
</NSwitch>
|
||||||
|
</template>
|
||||||
|
<DanmakuContainer :show-live-info="false" :current-danmakus="userDanmakus ?? []" :current-live="currentLive" :height="500" :show-border="false" :show-tools="modalShowTools" to="space" />
|
||||||
|
</NModal>
|
||||||
|
<NModal v-model:show="showExportModal" preset="card" style="width: 500px; max-width: 90vw; height: auto">
|
||||||
|
<template #header> 导出 </template>
|
||||||
|
<NSpin :show="isExporting">
|
||||||
|
<NSpace vertical align="center">
|
||||||
|
<NRadioGroup v-model:value="exportType" style="margin: 0 auto">
|
||||||
|
<NRadioButton value="json"> Json </NRadioButton>
|
||||||
|
<NRadioButton value="xml"> XML </NRadioButton>
|
||||||
|
<NRadioButton value="csv">
|
||||||
|
CSV
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NIcon>
|
||||||
|
<Info12Filled />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
只包含弹幕, 可在 Excel 中打开
|
||||||
|
</NTooltip>
|
||||||
|
</NRadioButton>
|
||||||
|
</NRadioGroup>
|
||||||
|
<NButton type="primary" size="large" @click="Export"> 导出 </NButton>
|
||||||
|
<span></span>
|
||||||
|
<NCollapse>
|
||||||
|
<NCollapseItem title="关于" name="1">
|
||||||
|
<div>
|
||||||
|
文件名格式: {<NTooltip><template #trigger>生成时间</template>Unix</NTooltip>}_{<NTooltip><template #trigger>开始时间</template>Unix: {{ currentLive.startAt }} </NTooltip>}_{<NTooltip
|
||||||
|
><template #trigger>直播间标题</template>' _ ' 将被转义为 ' - '</NTooltip
|
||||||
|
>}_{<NTooltip><template #trigger>用户名</template>{{ accountInfo?.name }}</NTooltip
|
||||||
|
>}.{{ exportType }}
|
||||||
|
<br />
|
||||||
|
弹幕Type对应:
|
||||||
|
<br />● 0 : 上舰 <br />● 1: sc <br />● 2: 礼物 <br />● 3: 弹幕
|
||||||
|
</div>
|
||||||
|
</NCollapseItem>
|
||||||
|
</NCollapse>
|
||||||
|
<span style="color: gray"> </span>
|
||||||
|
</NSpace>
|
||||||
|
</NSpin>
|
||||||
|
</NModal>
|
||||||
|
<NCard style="height: 100%" embedded :bordered="bordered" content-style="padding: 12px">
|
||||||
|
<template #header>
|
||||||
|
<slot name="header"> </slot>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<slot name="header-extra"> </slot>
|
||||||
|
</template>
|
||||||
|
<template v-if="showLiveInfo">
|
||||||
|
<LiveInfoContainer :live="currentLive" :show-liver="showLiver" show-area :show-statistic="showStatistic" />
|
||||||
|
<NDivider title-placement="left" style="font-size: 12px" v-if="toolsVisiable">
|
||||||
|
<NSwitch v-if="showRank" v-model:value="isRanking" @update-value="OnRank" size="small">
|
||||||
|
<template #checked> 排行 </template>
|
||||||
|
<template #unchecked> 弹幕 </template>
|
||||||
|
</NSwitch>
|
||||||
|
<NDivider v-if="showRank && !isRanking" vertical />
|
||||||
|
<Transition>
|
||||||
|
<span v-if="!isRanking">
|
||||||
|
<NSwitch v-model:value="innerShowTools" size="small">
|
||||||
|
<template #checked> 工具 </template>
|
||||||
|
<template #unchecked> 工具 </template>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon :component="Wrench24Filled" />
|
||||||
|
</template>
|
||||||
|
</NSwitch>
|
||||||
|
</span>
|
||||||
|
<span v-else></span>
|
||||||
|
</Transition>
|
||||||
|
</NDivider>
|
||||||
|
<br v-else />
|
||||||
|
</template>
|
||||||
|
<NCollapseTransition :show="innerShowTools && !isRanking">
|
||||||
|
<NSpace vertical style="padding-bottom: 16px">
|
||||||
|
<NSpace align="center">
|
||||||
|
<NButton type="primary" size="small" @click="showExportModal = true" @update:value="UpdateDanmakus"> 导出 </NButton>
|
||||||
|
<NCheckbox v-model:checked="orderDecreasing" @update:checked="UpdateDanmakus"> 降序 </NCheckbox>
|
||||||
|
<NCollapseTransition :show="filterSelected.includes(EventDataTypes.Message)">
|
||||||
|
<NCheckbox v-model:checked="hideEmoji" @update:checked="UpdateDanmakus"> 隐藏表情 </NCheckbox>
|
||||||
|
</NCollapseTransition>
|
||||||
|
<NCheckbox v-model:checked="hideAvatar"> 隐藏头像 </NCheckbox>
|
||||||
|
<NCheckbox v-model:checked="orderByPrice" @update:checked="UpdateDanmakus"> 按价格排序 </NCheckbox>
|
||||||
|
</NSpace>
|
||||||
|
<NSpace align="center">
|
||||||
|
<NInput v-model:value="keyword" size="small" style="max-width: 200px" placeholder="内容筛选" clearable @update:value="debouncedFn">
|
||||||
|
<template #prefix>
|
||||||
|
<NIcon :component="Search24Filled" />
|
||||||
|
</template>
|
||||||
|
</NInput>
|
||||||
|
<NCheckbox v-model:checked="enableRegex" @update:checked="UpdateDanmakus"> 正则 </NCheckbox>
|
||||||
|
<NCheckbox v-model:checked="deselect" @update:checked="UpdateDanmakus"> 反选 </NCheckbox>
|
||||||
|
</NSpace>
|
||||||
|
<NSpace align="center">
|
||||||
|
<NInputNumber v-model:value="price" placeholder="最低价格" size="small" style="max-width: 200px" clearable :min="0" @update:value="debouncedFn">
|
||||||
|
<template #prefix>
|
||||||
|
<NIcon :component="Money20Regular" />
|
||||||
|
</template>
|
||||||
|
</NInputNumber>
|
||||||
|
<NTag size="small" checkable :checked="price == 0.1" @update-checked="ChangePrice(0.1)"> 0.1 </NTag>
|
||||||
|
<NTag size="small" checkable :checked="price == 1" @update-checked="ChangePrice(1)"> 1 </NTag>
|
||||||
|
<NTag size="small" checkable :checked="price == 9.9" @update-checked="ChangePrice(9.9)"> 9.9 </NTag>
|
||||||
|
<NTag size="small" checkable :checked="price == 30" @update-checked="ChangePrice(30)"> 30 </NTag>
|
||||||
|
<NTag size="small" checkable :checked="price == 100" @update-checked="ChangePrice(100)"> 100 </NTag>
|
||||||
|
</NSpace>
|
||||||
|
<NCheckboxGroup v-model:value="filterSelected" @update:value="UpdateDanmakus">
|
||||||
|
<NSpace>
|
||||||
|
<NCheckbox :value="EventDataTypes.Message" label="弹幕" />
|
||||||
|
<NCheckbox :value="EventDataTypes.Gift" label="礼物" />
|
||||||
|
<NCheckbox :value="EventDataTypes.Guard" label="舰长" />
|
||||||
|
<NCheckbox :value="EventDataTypes.SC" label="Superchat" />
|
||||||
|
</NSpace>
|
||||||
|
</NCheckboxGroup>
|
||||||
|
</NSpace>
|
||||||
|
<NDivider style="margin-top: 0px; margin-bottom: 12px" title-placement="left">
|
||||||
|
<NTag style="font-size: 12px" size="small">
|
||||||
|
{{ danmakus.length }}
|
||||||
|
{{ danmakus.length != currentDanmakus.length ? `/ ${currentDanmakus.length}` : '' }}
|
||||||
|
条
|
||||||
|
</NTag>
|
||||||
|
<NDivider vertical />
|
||||||
|
<NTag style="font-size: 12px" size="small" type="error" :bordered="false">
|
||||||
|
💰
|
||||||
|
{{ RoundNumber(danmakus.reduce((a, b) => a + (b.price && b.price > 0 ? b.price : 0), 0)) }}
|
||||||
|
</NTag>
|
||||||
|
</NDivider>
|
||||||
|
</NCollapseTransition>
|
||||||
|
<div :style="isRanking ? 'display:none' : ''">
|
||||||
|
<SimpleVirtualList v-if="danmakus.length > itemRange" :items="danmakus" :default-size="itemHeight" :default-height="height ?? 1000">
|
||||||
|
<template #default="{ item }">
|
||||||
|
<p :style="`min-height: ${itemHeight}px;width:97%;display:flex;align-items:center;`">
|
||||||
|
<DanmakuItem :danmaku="item" :account-info="accountInfo" @on-click-name="OnNameClick" :show-name="showName" :show-avatar="!hideAvatar" :height="itemHeight" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</SimpleVirtualList>
|
||||||
|
<p v-else v-for="item in danmakus" :style="`min-height: ${itemHeight}px;width:97%;display:flex;align-items:center;`" v-bind:key="item.id">
|
||||||
|
<DanmakuItem :danmaku="item" :account-info="accountInfo" @on-click-name="OnNameClick" :show-name="showName" :show-avatar="!hideAvatar" :height="itemHeight" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="isRanking">
|
||||||
|
<NRadioGroup
|
||||||
|
v-model:value="rankType"
|
||||||
|
@update:value="
|
||||||
|
(type) => {
|
||||||
|
OnRankDirect(type, false)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<NRadioButton :value="RankType.Danmaku"> 弹幕 </NRadioButton>
|
||||||
|
<NRadioButton :value="RankType.Paid"> 付费 </NRadioButton>
|
||||||
|
</NRadioGroup>
|
||||||
|
<NDivider />
|
||||||
|
<NList :show-divider="false" style="background-color: rgba(255, 255, 255, 0)">
|
||||||
|
<NListItem v-for="user in currentRankInfo" v-bind:key="user.uId">
|
||||||
|
<span style="display: flex; align-items: center">
|
||||||
|
<NAvatar round size="small" :style="GetRankIndexColor(user.Index)">
|
||||||
|
{{ user.Index }}
|
||||||
|
</NAvatar>
|
||||||
|
<NDivider vertical />
|
||||||
|
<NButton text type="info" @click="OnNameClick(user.uId)">
|
||||||
|
<NTooltip v-if="user.uId == accountInfo?.biliId">
|
||||||
|
<template #trigger>
|
||||||
|
<NTag size="small" type="warning" style="cursor: pointer">
|
||||||
|
{{ user.uName }}
|
||||||
|
</NTag>
|
||||||
|
</template>
|
||||||
|
主播
|
||||||
|
</NTooltip>
|
||||||
|
<template v-else>
|
||||||
|
{{ user.uName }}
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NDivider vertical />
|
||||||
|
<span v-if="rankType == RankType.Danmaku"> {{ user.Danmakus }} 条 </span>
|
||||||
|
<span v-else-if="rankType == RankType.Paid">
|
||||||
|
<NTag size="small" type="error" :bordered="false">
|
||||||
|
<NIcon :component="Money24Regular" />
|
||||||
|
{{ RoundNumber(user.Paid) }}
|
||||||
|
</NTag>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</NListItem>
|
||||||
|
</NList>
|
||||||
|
</div>
|
||||||
|
</NCard>
|
||||||
|
</NSpin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vListItem {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
169
src/components/DanmakuItem.vue
Normal file
169
src/components/DanmakuItem.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NButton, NDivider, NTooltip, NTag, NIcon, NCard } from 'naive-ui'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { Money24Regular, VehicleShip24Filled } from '@vicons/fluent'
|
||||||
|
import { AccountInfo, DanmakuModel, EventDataTypes } from '@/api/api-models'
|
||||||
|
|
||||||
|
function GetSCColor(price: number): string {
|
||||||
|
if (price === 0) return `#2a60b2`
|
||||||
|
if (price > 0 && price < 30) return `#2a60b2`
|
||||||
|
if (price >= 30 && price < 50) return `#2a60b2`
|
||||||
|
if (price >= 50 && price < 100) return `#427d9e`
|
||||||
|
if (price >= 100 && price < 500) return `#c99801`
|
||||||
|
if (price >= 500 && price < 1000) return `#e09443`
|
||||||
|
if (price >= 1000 && price < 2000) return `#e54d4d`
|
||||||
|
if (price >= 2000) return `#ab1a32`
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
function GetGuardColor(price: number | null | undefined): string {
|
||||||
|
if (price) {
|
||||||
|
if (price < 138) return ''
|
||||||
|
if (price >= 138 && price < 1598) return 'rgb(104, 136, 241)'
|
||||||
|
if (price >= 1598 && price < 15998) return 'rgb(157, 155, 255)'
|
||||||
|
if (price >= 15998) return 'rgb(122, 4, 35)'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
danmaku,
|
||||||
|
accountInfo,
|
||||||
|
height = 30,
|
||||||
|
showName = true,
|
||||||
|
showAvatar = true,
|
||||||
|
} = defineProps<{
|
||||||
|
danmaku: DanmakuModel
|
||||||
|
accountInfo: AccountInfo | undefined
|
||||||
|
showName?: boolean
|
||||||
|
showAvatar?: boolean
|
||||||
|
height?: number
|
||||||
|
}>()
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'onClickName', uId: number): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NCard
|
||||||
|
v-if="danmaku.type == EventDataTypes.SC"
|
||||||
|
:style="`margin-top: 5px;margin-bottom: 5px;max-width:500px;background-color: ${GetSCColor(danmaku.price ?? 0)};`"
|
||||||
|
content-style="border-radius: 3px;padding:5px;min-height:45px;display:flex;align-items:center;"
|
||||||
|
:header-style="`padding:5px;background: rgba(255, 255, 255, 15%);font-size: 14px;`"
|
||||||
|
size="small"
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<span style="display: flex; align-items: center; gap: 8px 8px">
|
||||||
|
<NTooltip v-if="danmaku.uId > 0 && showAvatar">
|
||||||
|
<template #trigger>
|
||||||
|
<img :src="`https://workers.vrp.moe/api/bilibili/avatar/${danmaku.uId}?size=25`" alt="头像" referrerpolicy="no-referrer" style="border-radius: 50%" loading="lazy" />
|
||||||
|
</template>
|
||||||
|
<img :src="`https://workers.vrp.moe/api/bilibili/avatar/${danmaku.uId}?size=1024`" alt="头像" referrerpolicy="no-referrer" loading="lazy" />
|
||||||
|
</NTooltip>
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<span style="color: white">
|
||||||
|
{{ format(danmaku.time, 'HH:mm:ss') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
{{ format(danmaku.time, 'yyyy-MM-dd HH:mm:ss') }}
|
||||||
|
</NTooltip>
|
||||||
|
<NButton v-if="showName" text type="primary" @click="$emit('onClickName', danmaku.uId)">
|
||||||
|
<NTag v-if="danmaku.uId == accountInfo?.biliId" size="small" type="warning">
|
||||||
|
{{ danmaku.uName }}
|
||||||
|
</NTag>
|
||||||
|
<template v-else>
|
||||||
|
<span style="color: white; font-weight: bold; text-shadow: rgb(124 59 59) 2px 2px 1px">
|
||||||
|
{{ danmaku.uName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
<NTag
|
||||||
|
size="small"
|
||||||
|
:style="`display:flex;margin-left: auto;background-color: ${GetSCColor(danmaku.price ?? 0)};color: #e1e1e1;min-width: 35px;justify-content:center;text-shadow: rgb(124 59 59) 2px 2px 1px;`"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
{{ danmaku.price }}
|
||||||
|
</NTag>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span style="color: white">
|
||||||
|
{{ danmaku.msg }}
|
||||||
|
</span>
|
||||||
|
</NCard>
|
||||||
|
<template v-else>
|
||||||
|
<span class="danmaku-item" style="display: flex; align-items: center; white-space: nowrap; margin-left: 5px; color: gray">
|
||||||
|
<NTooltip v-if="danmaku.uId > 0 && showAvatar">
|
||||||
|
<template #trigger>
|
||||||
|
<img :src="`https://workers.vrp.moe/api/bilibili/avatar/${danmaku.uId}?size=22`" alt="头像" referrerpolicy="no-referrer" style="border-radius: 50%; margin-right: 5px" />
|
||||||
|
</template>
|
||||||
|
<img :src="`https://workers.vrp.moe/api/bilibili/avatar/${danmaku.uId}?size=1024`" alt="头像" referrerpolicy="no-referrer" />
|
||||||
|
</NTooltip>
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
{{ format(danmaku.time, 'HH:mm:ss') }}
|
||||||
|
</template>
|
||||||
|
{{ format(danmaku.time, 'yyyy-MM-dd HH:mm:ss') }}
|
||||||
|
</NTooltip>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<template v-if="showName && danmaku.uId != -1">
|
||||||
|
<NButton class="danmaku-item" text type="info" @click="$emit('onClickName', danmaku.uId)">
|
||||||
|
<NTooltip v-if="danmaku.uId == accountInfo?.biliId">
|
||||||
|
<template #trigger>
|
||||||
|
<NTag size="small" type="warning" style="cursor: pointer">
|
||||||
|
{{ danmaku.uName && danmaku.uName != '' ? danmaku.uName : '主播' }}
|
||||||
|
</NTag>
|
||||||
|
</template>
|
||||||
|
主播
|
||||||
|
</NTooltip>
|
||||||
|
<template v-else>
|
||||||
|
<span :style="danmaku.uName != '' && !showAvatar ? 'min-width: 60px' : ''">
|
||||||
|
{{ danmaku.uName }}
|
||||||
|
<span style="color: gray">
|
||||||
|
{{ ': ' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
<span v-if="danmaku.type == EventDataTypes.Message">
|
||||||
|
<template v-if="danmaku.isEmoji">
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<img :src="'https://' + danmaku.msg + `@22h`" referrerpolicy="no-referrer" :style="`max-height: ${height}px;display:inline-flex;`" />
|
||||||
|
</template>
|
||||||
|
<img :src="'https://' + danmaku.msg ?? ''" referrerpolicy="no-referrer" />
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ danmaku.msg }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="danmaku.type == EventDataTypes.Gift" :style="'color:' + ((danmaku.price ?? 0) > 0 ? '#DD2F2F' : '#E9A8A8')">
|
||||||
|
<NTag size="tiny" v-if="(danmaku.price ?? 0) > 0" type="error" :bordered="false"> <NIcon :component="Money24Regular" /> {{ danmaku.price }} </NTag>
|
||||||
|
{{ danmaku.price ?? 0 > 0 ? '' : '免费' }}礼物
|
||||||
|
<NDivider vertical />
|
||||||
|
{{ danmaku.msg }}
|
||||||
|
<NTag size="tiny" :bordered="false" v-if="danmaku.num">
|
||||||
|
<span style="color: gray"> {{ danmaku.num }} 个 </span>
|
||||||
|
</NTag>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="danmaku.type == EventDataTypes.Guard" style="color: #9d78c1">
|
||||||
|
上舰
|
||||||
|
<NTag size="small" :style="`color:${GetGuardColor(danmaku.price ?? 0)}`"> <NIcon :component="VehicleShip24Filled" /> {{ danmaku.price }} </NTag>
|
||||||
|
<NDivider vertical />
|
||||||
|
{{ danmaku.msg }}
|
||||||
|
</span>
|
||||||
|
<span v-else> {{ danmaku.msg }} default </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.danmaku-item {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/components/LiveInfoContainer.vue
Normal file
202
src/components/LiveInfoContainer.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { EventDataTypes, ResponseLiveInfoModel } from '@/api/api-models'
|
||||||
|
import { Info24Filled } from '@vicons/fluent'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
import { NPopover, NSpace, NStatistic, NTime, NDivider, NNumberAnimation, NTag, NButton, NTooltip, NIcon } from 'naive-ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { live } = defineProps<{
|
||||||
|
live: ResponseLiveInfoModel
|
||||||
|
}>()
|
||||||
|
|
||||||
|
let defaultDanmakusCount = ref(0)
|
||||||
|
function OnClickCover() {
|
||||||
|
router.push({
|
||||||
|
name: 'manage-liveDetail',
|
||||||
|
params: { id: live.liveId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => live,
|
||||||
|
(newValue) => {
|
||||||
|
defaultDanmakusCount.value = newValue.danmakusCount
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="display: flex; flex-wrap: wrap">
|
||||||
|
<NSpace style="flex-flow: nowrap">
|
||||||
|
<span style="display: flex; align-items: center; height: 100%">
|
||||||
|
<img
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
:style="!live.isFinish ? 'animation: animated-border 2.5s infinite;cursor: pointer' : 'cursor: pointer'"
|
||||||
|
class="liveCover"
|
||||||
|
:src="live.coverUrl + '@200w'"
|
||||||
|
lazy
|
||||||
|
preview-disabled
|
||||||
|
@click="OnClickCover()"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<NSpace vertical justify="center" style="gap: 2px">
|
||||||
|
<NButton text @click="OnClickCover()">
|
||||||
|
<span style="font-size: 18px; white-space: break-spaces">
|
||||||
|
{{ live.title }}
|
||||||
|
</span>
|
||||||
|
</NButton>
|
||||||
|
<span>
|
||||||
|
<span v-if="!live.isFinish">
|
||||||
|
<NTag size="tiny" :bordered="false" type="success" style="justify-items: center; box-shadow: 0 0 3px #589580"> 直播中 </NTag>
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: gray">
|
||||||
|
{{ (((live.stopAt ?? 0) - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||||
|
时
|
||||||
|
</span>
|
||||||
|
<NDivider vertical />
|
||||||
|
<NPopover trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<div style="color: grey; font-size: small; display: inline">
|
||||||
|
<NTime style="font-size: small" :time="live.startAt" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-if="live.isFinish">
|
||||||
|
结束于:
|
||||||
|
<NTime :time="live.stopAt ?? 0" />
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
已直播:
|
||||||
|
{{ ((Date.now() - (live.stopAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||||
|
时
|
||||||
|
</span>
|
||||||
|
</NPopover>
|
||||||
|
</span>
|
||||||
|
</NSpace>
|
||||||
|
</NSpace>
|
||||||
|
<div class="liveListItem">
|
||||||
|
<NStatistic label="分区">
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<span style="font-size: 16px; font-weight: 500">
|
||||||
|
{{ live.area }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
{{ live.parentArea }}
|
||||||
|
</NTooltip>
|
||||||
|
</NStatistic>
|
||||||
|
<NStatistic label="弹幕">
|
||||||
|
<span style="font-size: 18px; font-weight: 500">
|
||||||
|
<NNumberAnimation :from="defaultDanmakusCount" :to="live.danmakusCount" show-separator />
|
||||||
|
</span>
|
||||||
|
</NStatistic>
|
||||||
|
<NStatistic label="互动" tabular-nums>
|
||||||
|
<span style="font-size: 18px; font-weight: 500">
|
||||||
|
<NNumberAnimation :from="0" :to="live.interactionCount" show-separator />
|
||||||
|
</span>
|
||||||
|
</NStatistic>
|
||||||
|
<transition>
|
||||||
|
<NStatistic tabular-nums>
|
||||||
|
<template #label>
|
||||||
|
收益
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NIcon :component="Info24Filled" />
|
||||||
|
</template>
|
||||||
|
因为官方并没有提供上舰的价格, 所以记录中的舰长价格一律按照打折价格计算
|
||||||
|
<br />
|
||||||
|
即舰长 138, 提督 1598, 总督 15998
|
||||||
|
<br />
|
||||||
|
把鼠标放在下面的价格上就可以查看排除舰长后的收益
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<span style="font-size: 18px; font-weight: 500; color: #a35353">
|
||||||
|
<NNumberAnimation :from="0" :to="live.totalIncomeWithGuard" show-separator />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<NNumberAnimation :from="0" :to="live.totalIncome" show-separator />
|
||||||
|
</NTooltip>
|
||||||
|
</NStatistic>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-statistic {
|
||||||
|
text-align: right;
|
||||||
|
min-width: 62px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 750px) {
|
||||||
|
.liveCover {
|
||||||
|
width: 90px;
|
||||||
|
height: fit-content;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liveList {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liveListItem {
|
||||||
|
padding-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateEChartStyle {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 750px) {
|
||||||
|
.liveCover {
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liveList {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liveListItem {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateEChartStyle {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animated-border {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0px #589580;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
84
src/components/SimpleVirtualList.vue
Normal file
84
src/components/SimpleVirtualList.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { VVirtualList } from 'vueuc'
|
||||||
|
import type { VirtualListInst } from 'vueuc'
|
||||||
|
import { NScrollbar } from 'naive-ui/es/_internal'
|
||||||
|
import type { ScrollbarInst } from 'naive-ui/es/_internal'
|
||||||
|
import { computed, onMounted, ref, type PropType } from 'vue'
|
||||||
|
import type { ItemData } from 'vueuc/lib/virtual-list/src/type'
|
||||||
|
|
||||||
|
const scrollerInstRef = ref<ScrollbarInst | null>(null)
|
||||||
|
const vlInstRef = ref<VirtualListInst | null>(null)
|
||||||
|
|
||||||
|
function scrollContainer(): HTMLElement | null {
|
||||||
|
const { value } = vlInstRef
|
||||||
|
if (!value) return null
|
||||||
|
const { listElRef } = value
|
||||||
|
return listElRef
|
||||||
|
}
|
||||||
|
function scrollContent(): HTMLElement | null {
|
||||||
|
const { value } = vlInstRef
|
||||||
|
if (!value) return null
|
||||||
|
const { itemsElRef } = value
|
||||||
|
return itemsElRef
|
||||||
|
}
|
||||||
|
function syncVLScroller(): void {
|
||||||
|
scrollerInstRef.value?.sync()
|
||||||
|
}
|
||||||
|
function ScrollTo(to: { position: 'top' | 'bottom'; behavior?: ScrollBehavior; debounce?: boolean }) {
|
||||||
|
scrollerInstRef.value?.scrollTo(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<ItemData[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
defaultSize: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
defaultHeight: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
scrollToEndDefault: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.scrollToEndDefault) {
|
||||||
|
scrollerInstRef.value?.scrollTo({
|
||||||
|
position: 'bottom',
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentHeight = computed(() => {
|
||||||
|
return scrollerInstRef.value?.$el.parentElement?.clientHeight ?? 0
|
||||||
|
})
|
||||||
|
const height = computed(() => {
|
||||||
|
if (typeof props.defaultHeight == 'number') return (props.defaultHeight < 0 ? parentHeight : props.defaultHeight) + 'px'
|
||||||
|
else {
|
||||||
|
if (props.defaultHeight.endsWith('%')) {
|
||||||
|
return parentHeight.value * (Number(props.defaultHeight.replace('%', '')) / 100) + 'px'
|
||||||
|
} else if (props.defaultHeight.endsWith('vh') || props.defaultHeight.endsWith('vw')) {
|
||||||
|
return props.defaultHeight
|
||||||
|
} else {
|
||||||
|
console.log(`[SimpleVirtualList] Invalid height value: ${props.defaultHeight}`)
|
||||||
|
return 0 + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NScrollbar ref="scrollerInstRef" :style="'height:' + height" :container="scrollContainer" :content="scrollContent" trigger="none">
|
||||||
|
<VVirtualList ref="vlInstRef" :items="items" :item-size="defaultSize" item-resizable key-field="id" :show-scrollbar="false" @scroll="syncVLScroller">
|
||||||
|
<template #default="{ item }">
|
||||||
|
<slot :item="item"> </slot>
|
||||||
|
</template>
|
||||||
|
</VVirtualList>
|
||||||
|
</NScrollbar>
|
||||||
|
</template>
|
||||||
117
src/data/DanmakuExport.ts
Normal file
117
src/data/DanmakuExport.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { AccountInfo, DanmakuModel, EventDataTypes, ResponseLiveInfoModel } from '@/api/api-models'
|
||||||
|
import { XMLBuilder } from 'fast-xml-parser'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
|
||||||
|
const builder = new XMLBuilder({
|
||||||
|
attributeNamePrefix: '@',
|
||||||
|
ignoreAttributes: false,
|
||||||
|
processEntities: false,
|
||||||
|
format: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function GetString(account: AccountInfo | undefined, live: ResponseLiveInfoModel, danmakus: DanmakuModel[], type: 'json' | 'xml' | 'csv') {
|
||||||
|
const tempDanmakus = new List(danmakus)
|
||||||
|
.Select((d) => {
|
||||||
|
return {
|
||||||
|
uId: d.uId,
|
||||||
|
uName: d.uName,
|
||||||
|
sendDate: d.time,
|
||||||
|
type: d.type,
|
||||||
|
message: d.msg,
|
||||||
|
price: d.price,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
const obj = {
|
||||||
|
live: live,
|
||||||
|
danmakus: tempDanmakus,
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'json': {
|
||||||
|
return JSON.stringify(obj)
|
||||||
|
}
|
||||||
|
case 'xml': {
|
||||||
|
const xmlJsonObj = {
|
||||||
|
i: {
|
||||||
|
chatserver: 'chat.bilibili.com',
|
||||||
|
chatid: '0',
|
||||||
|
mission: '0',
|
||||||
|
maxlimit: '0',
|
||||||
|
state: '0',
|
||||||
|
real_name: '0',
|
||||||
|
source: 'e-r',
|
||||||
|
metadata: {
|
||||||
|
user_name: account?.name,
|
||||||
|
room_id: account?.biliRoomId,
|
||||||
|
room_title: live.title,
|
||||||
|
area: live.area,
|
||||||
|
parent_area: live.parentArea,
|
||||||
|
live_start_time: new Date(live.startAt),
|
||||||
|
record_start_time: new Date(live.stopAt ?? 0),
|
||||||
|
recorder: 'https://vtsuru.live/',
|
||||||
|
},
|
||||||
|
d: [] as any[],
|
||||||
|
gift: [] as any[],
|
||||||
|
sc: [] as any[],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
danmakus.forEach((danmaku) => {
|
||||||
|
if (danmaku.type == EventDataTypes.Message) {
|
||||||
|
xmlJsonObj.i.d.push({
|
||||||
|
'@p': `${GetTime(danmaku)},1,25,16777215,${danmaku.time},0,-1`,
|
||||||
|
'@uid': danmaku.uId,
|
||||||
|
'@user': danmaku.uName,
|
||||||
|
'#text': danmaku.msg,
|
||||||
|
})
|
||||||
|
} else if (danmaku.type == EventDataTypes.Gift) {
|
||||||
|
xmlJsonObj.i.gift.push({
|
||||||
|
'@ts': GetTime(danmaku),
|
||||||
|
'@uid': danmaku.uId,
|
||||||
|
'@user': danmaku.uName,
|
||||||
|
'@giftname': danmaku.msg,
|
||||||
|
'@giftcount': danmaku.num,
|
||||||
|
'@cointype': (danmaku.price ?? 0) > 0 ? '金瓜子' : '银瓜子',
|
||||||
|
'@price': (danmaku.price ?? 0) * 1000,
|
||||||
|
})
|
||||||
|
} else if (danmaku.type == EventDataTypes.Guard) {
|
||||||
|
xmlJsonObj.i.gift.push({
|
||||||
|
'@ts': GetTime(danmaku),
|
||||||
|
'@uid': danmaku.uId,
|
||||||
|
'@user': danmaku.uName,
|
||||||
|
'@giftname': danmaku.msg,
|
||||||
|
'@giftcount': danmaku.num,
|
||||||
|
'@cointype': '舰长',
|
||||||
|
'@price': danmaku.price,
|
||||||
|
})
|
||||||
|
} else if (danmaku.type == EventDataTypes.SC) {
|
||||||
|
xmlJsonObj.i.sc.push({
|
||||||
|
'@ts': GetTime(danmaku),
|
||||||
|
'@uid': danmaku.uId,
|
||||||
|
'@user': danmaku.uName,
|
||||||
|
'@price': danmaku.price,
|
||||||
|
'#text': danmaku.msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
function GetTime(danmaku: DanmakuModel) {
|
||||||
|
return ((danmaku.time - live.startAt) / 1000).toFixed(3)
|
||||||
|
}
|
||||||
|
return builder.build(xmlJsonObj)
|
||||||
|
}
|
||||||
|
case 'csv': {
|
||||||
|
return objectsToCSV(tempDanmakus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function objectsToCSV(arr: any[]) {
|
||||||
|
const array = [Object.keys(arr[0])].concat(arr)
|
||||||
|
return array
|
||||||
|
.map((row) => {
|
||||||
|
return Object.values(row)
|
||||||
|
.map((value) => {
|
||||||
|
return typeof value === 'string' ? JSON.stringify(value) : value
|
||||||
|
})
|
||||||
|
.toString()
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export const OPEN_LIVE_API_URL = `${BASE_API}open-live/`
|
|||||||
export const SONG_REQUEST_API_URL = `${BASE_API}song-request/`
|
export const SONG_REQUEST_API_URL = `${BASE_API}song-request/`
|
||||||
export const QUEUE_API_URL = `${BASE_API}queue/`
|
export const QUEUE_API_URL = `${BASE_API}queue/`
|
||||||
export const EVENT_API_URL = `${BASE_API}event/`
|
export const EVENT_API_URL = `${BASE_API}event/`
|
||||||
|
export const LIVE_API_URL = `${BASE_API}live/`
|
||||||
|
|
||||||
export const ScheduleTemplateMap = {
|
export const ScheduleTemplateMap = {
|
||||||
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')) },
|
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/scheduleTemplate/DefaultScheduleTemplate.vue')) },
|
||||||
|
|||||||
@@ -210,6 +210,24 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
danmaku: true,
|
danmaku: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'live',
|
||||||
|
name: 'manage-live',
|
||||||
|
component: () => import('@/views/manage/LiveManager.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '直播记录',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'live/:id',
|
||||||
|
name: 'manage-liveDetail',
|
||||||
|
component: () => import('@/views/manage/LiveDetailManage.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '直播详情',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
|||||||
</NSpace>
|
</NSpace>
|
||||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||||
<NTimeline>
|
<NTimeline>
|
||||||
|
<NTimelineItem type="success" title="功能添加" content="直播记录" time="2023-12-3" />
|
||||||
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
<NTimelineItem type="info" title="功能更新" content="歌单添加 '简单' 模板" time="2023-11-30" />
|
||||||
<NTimelineItem type="success" title="功能添加" content="排队" time="2023-11-25" />
|
<NTimelineItem type="success" title="功能添加" content="排队" time="2023-11-25" />
|
||||||
<NTimelineItem type="success" title="功能添加" content="点歌" time="2023-11-20" />
|
<NTimelineItem type="success" title="功能添加" content="点歌" time="2023-11-20" />
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ const functions = [
|
|||||||
desc: '能够记录并查询上舰和SC记录',
|
desc: '能够记录并查询上舰和SC记录',
|
||||||
icon: VehicleShip24Filled,
|
icon: VehicleShip24Filled,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '直播场次记录',
|
||||||
|
desc: '记录每场直播的数据以及弹幕等内容',
|
||||||
|
icon: VehicleShip24Filled,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '日程表',
|
name: '日程表',
|
||||||
desc: '提供多种样式的日程表',
|
desc: '提供多种样式的日程表',
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { h, onMounted, ref } from 'vue'
|
import { h, onMounted, ref } from 'vue'
|
||||||
import { BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, AnalyticsSharp } from '@vicons/ionicons5'
|
import { BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, AnalyticsSharp } from '@vicons/ionicons5'
|
||||||
import { CalendarClock24Filled, Chat24Filled, Info24Filled, Lottery24Filled, PeopleQueue24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
import { CalendarClock24Filled, Chat24Filled, Info24Filled, Live24Filled, Lottery24Filled, PeopleQueue24Filled, VehicleShip24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||||
import { isLoadingAccount, useAccount } from '@/api/account'
|
import { isLoadingAccount, useAccount } from '@/api/account'
|
||||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
@@ -86,6 +86,21 @@ const menuOptions = [
|
|||||||
disabled: accountInfo.value?.isEmailVerified == false,
|
disabled: accountInfo.value?.isEmailVerified == false,
|
||||||
icon: renderIcon(VehicleShip24Filled),
|
icon: renderIcon(VehicleShip24Filled),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () =>
|
||||||
|
h(
|
||||||
|
RouterLink,
|
||||||
|
{
|
||||||
|
to: {
|
||||||
|
name: 'manage-live',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ default: () => '直播记录' }
|
||||||
|
),
|
||||||
|
key: 'manage-live',
|
||||||
|
disabled: accountInfo.value?.isEmailVerified == false,
|
||||||
|
icon: renderIcon(Live24Filled),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () =>
|
label: () =>
|
||||||
h(
|
h(
|
||||||
|
|||||||
62
src/views/manage/LiveDetailManage.vue
Normal file
62
src/views/manage/LiveDetailManage.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccount } from '@/api/account'
|
||||||
|
import { DanmakuModel, ResponseLiveInfoModel } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI } from '@/api/query'
|
||||||
|
import DanmakuContainer from '@/components/DanmakuContainer.vue'
|
||||||
|
import { LIVE_API_URL } from '@/data/constants'
|
||||||
|
import { NButton, useMessage } from 'naive-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
interface ResponseLiveDetail {
|
||||||
|
live: ResponseLiveInfoModel
|
||||||
|
danmakus: DanmakuModel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountInfo = useAccount()
|
||||||
|
const message = useMessage()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const liveInfo = ref<ResponseLiveDetail | undefined>(await get())
|
||||||
|
|
||||||
|
async function get() {
|
||||||
|
try {
|
||||||
|
const data = await QueryGetAPI<ResponseLiveDetail>(LIVE_API_URL + 'get', {
|
||||||
|
id: route.params.id,
|
||||||
|
useEmoji: true
|
||||||
|
})
|
||||||
|
if (data.code == 200) {
|
||||||
|
return data.data
|
||||||
|
} else {
|
||||||
|
message.error('无法获取数据: ' + data.message)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('无法获取数据')
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NButton @click="router.go(-1)" text>
|
||||||
|
{{ '< 返回' }}
|
||||||
|
</NButton>
|
||||||
|
<DanmakuContainer
|
||||||
|
v-if="liveInfo"
|
||||||
|
ref="danmakuContainerRef"
|
||||||
|
:current-live="liveInfo.live"
|
||||||
|
:current-danmakus="liveInfo.danmakus"
|
||||||
|
:height="750"
|
||||||
|
show-rank
|
||||||
|
show-liver
|
||||||
|
show-live-info
|
||||||
|
show-tools
|
||||||
|
show-name
|
||||||
|
to="userDanmakus"
|
||||||
|
:item-range="100"
|
||||||
|
:item-height="25"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
68
src/views/manage/LiveManager.vue
Normal file
68
src/views/manage/LiveManager.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAccount } from '@/api/account'
|
||||||
|
import { ResponseLiveInfoModel } from '@/api/api-models'
|
||||||
|
import { QueryGetAPI } from '@/api/query'
|
||||||
|
import LiveInfoContainer from '@/components/LiveInfoContainer.vue'
|
||||||
|
import { LIVE_API_URL } from '@/data/constants'
|
||||||
|
import { List } from 'linqts'
|
||||||
|
import { NButton, NDivider, NList, NListItem, NAlert, NPagination, NPopover, NSpace, NStatistic, NTag, NTime, NTooltip, useMessage } from 'naive-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const accountInfo = useAccount()
|
||||||
|
const message = useMessage()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const lives = ref<ResponseLiveInfoModel[]>(await getAll())
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const defaultDanmakusCount = ref(0)
|
||||||
|
|
||||||
|
async function getAll() {
|
||||||
|
try {
|
||||||
|
const data = await QueryGetAPI<ResponseLiveInfoModel[]>(LIVE_API_URL + 'get-all')
|
||||||
|
if (data.code == 200) {
|
||||||
|
return data.data
|
||||||
|
} else {
|
||||||
|
message.error('无法获取数据: ' + data.message)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('无法获取数据')
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnClickCover(live: ResponseLiveInfoModel) {
|
||||||
|
router.push({
|
||||||
|
name: 'manage-liveDetail',
|
||||||
|
params: { id: live.liveId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NSpace vertical>
|
||||||
|
<NAlert type="warning"> 测试功能, 尚不稳定 </NAlert>
|
||||||
|
<NAlert type="info">
|
||||||
|
当前本站正在测试直接从服务端记录并储存数据, 不过并不清楚B站的风控策略, 此功能不一定会长期有效
|
||||||
|
<br />
|
||||||
|
在我们被限制连接之前无需部署 VtsuruEventFetcher 即可使用相关功能 (如记录上舰和SC以及直播记录) 😊
|
||||||
|
</NAlert>
|
||||||
|
</NSpace>
|
||||||
|
<br />
|
||||||
|
<NAlert v-if="accountInfo?.isBiliVerified != true" type="info"> 尚未进行Bilibili认证 </NAlert>
|
||||||
|
<template v-else>
|
||||||
|
<NSpace vertical justify="center" align="center">
|
||||||
|
<NPagination v-model:page="page" show-quick-jumper show-size-picker :page-sizes="[10, 20, 30, 40]" :item-count="lives.length" />
|
||||||
|
</NSpace>
|
||||||
|
<NDivider />
|
||||||
|
<NList bordered hoverable clickable>
|
||||||
|
<NListItem @click="OnClickCover(live)" v-for="live in lives" v-bind:key="live.liveId">
|
||||||
|
<LiveInfoContainer :live="live" />
|
||||||
|
</NListItem>
|
||||||
|
</NList>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -335,6 +335,7 @@ async function updateSongStatus(song: SongRequestInfo, status: SongRequestStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onGetDanmaku(danmaku: DanmakuInfo) {
|
function onGetDanmaku(danmaku: DanmakuInfo) {
|
||||||
|
console.log(danmaku)
|
||||||
if (checkMessage(danmaku.msg)) {
|
if (checkMessage(danmaku.msg)) {
|
||||||
addSong({
|
addSong({
|
||||||
msg: danmaku.msg,
|
msg: danmaku.msg,
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -1698,6 +1698,13 @@ fast-unique-numbers@^8.0.11:
|
|||||||
"@babel/runtime" "^7.23.4"
|
"@babel/runtime" "^7.23.4"
|
||||||
tslib "^2.6.2"
|
tslib "^2.6.2"
|
||||||
|
|
||||||
|
fast-xml-parser@^4.3.2:
|
||||||
|
version "4.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz#761e641260706d6e13251c4ef8e3f5694d4b0d79"
|
||||||
|
integrity sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==
|
||||||
|
dependencies:
|
||||||
|
strnum "^1.0.5"
|
||||||
|
|
||||||
fastq@^1.6.0:
|
fastq@^1.6.0:
|
||||||
version "1.15.0"
|
version "1.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
||||||
@@ -3017,6 +3024,11 @@ strip-json-comments@^3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||||
|
|
||||||
|
strnum@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
|
||||||
|
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
|
||||||
|
|
||||||
stylus@^0.55.0:
|
stylus@^0.55.0:
|
||||||
version "0.55.0"
|
version "0.55.0"
|
||||||
resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.55.0.tgz#bd404a36dd93fa87744a9dd2d2b1b8450345e5fc"
|
resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.55.0.tgz#bd404a36dd93fa87744a9dd2d2b1b8450345e5fc"
|
||||||
|
|||||||
Reference in New Issue
Block a user