This commit is contained in:
2023-12-03 15:14:02 +08:00
parent cfbfaf7938
commit f7d3a2128a
16 changed files with 1334 additions and 1 deletions

View 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>

View 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>

View 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>

View 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>