chore: format code style and update linting configuration

This commit is contained in:
Megghy
2025-10-02 10:38:23 +08:00
parent 6fd046adcd
commit 758549d29d
253 changed files with 16258 additions and 15833 deletions

View File

@@ -1,3 +1,33 @@
<script setup>
import { NTooltip } from 'naive-ui'
import { computed } from 'vue'
import { FILE_BASE_URL } from '@/data/constants'
import * as constants from './constants'
const props = defineProps({
isAdmin: Boolean,
privilegeType: Number,
})
const authorTypeText = computed(() => {
if (props.isAdmin) {
return 'moderator'
}
return props.privilegeType > 0 ? 'member' : ''
})
const readableAuthorTypeText = computed(() => {
if (props.isAdmin) {
return '管理员'
}
return constants.getShowGuardLevelText(props.privilegeType)
})
const fileServerUrl = computed(() => {
return FILE_BASE_URL
})
</script>
<template>
<yt-live-chat-author-badge-renderer :type="authorTypeText">
<NTooltip
@@ -41,35 +71,6 @@
</yt-live-chat-author-badge-renderer>
</template>
<script setup>
import { computed } from 'vue'
import { NTooltip } from 'naive-ui'
import * as constants from './constants'
import { FILE_BASE_URL } from '@/data/constants'
const props = defineProps({
isAdmin: Boolean,
privilegeType: Number
})
const authorTypeText = computed(() => {
if (props.isAdmin) {
return 'moderator'
}
return props.privilegeType > 0 ? 'member' : ''
})
const readableAuthorTypeText = computed(() => {
if (props.isAdmin) {
return '管理员'
}
return constants.getShowGuardLevelText(props.privilegeType)
})
const fileServerUrl = computed(() => {
return FILE_BASE_URL
})
</script>
<style src="@/assets/css/youtube/yt-live-chat-author-badge-renderer.css"></style>
<style src="@/assets/css/youtube/yt-icon.css"></style>

View File

@@ -1,3 +1,22 @@
<script setup>
import { computed } from 'vue'
import AuthorBadge from './AuthorBadge.vue'
import * as constants from './constants'
const props = defineProps({
isInMemberMessage: Boolean,
authorName: String,
authorType: Number,
privilegeType: Number,
})
const AUTHOR_TYPE_ADMIN = constants.AUTHOR_TYPE_ADMIN
const authorTypeText = computed(() => {
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
})
</script>
<template>
<yt-live-chat-author-chip>
<span
@@ -18,20 +37,20 @@
id="chat-badges"
class="style-scope yt-live-chat-author-chip"
>
<author-badge
<AuthorBadge
v-if="isInMemberMessage"
class="style-scope yt-live-chat-author-chip"
:is-admin="false"
:privilege-type="privilegeType"
/>
<template v-else>
<author-badge
<AuthorBadge
v-if="authorType === AUTHOR_TYPE_ADMIN"
class="style-scope yt-live-chat-author-chip"
is-admin
:privilege-type="0"
/>
<author-badge
<AuthorBadge
v-if="privilegeType > 0"
class="style-scope yt-live-chat-author-chip"
:is-admin="false"
@@ -42,23 +61,4 @@
</yt-live-chat-author-chip>
</template>
<script setup>
import { computed } from 'vue'
import AuthorBadge from './AuthorBadge.vue'
import * as constants from './constants'
const props = defineProps({
isInMemberMessage: Boolean,
authorName: String,
authorType: Number,
privilegeType: Number
})
const AUTHOR_TYPE_ADMIN = constants.AUTHOR_TYPE_ADMIN
const authorTypeText = computed(() => {
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
})
</script>
<style src="@/assets/css/youtube/yt-live-chat-author-chip.css"></style>

View File

@@ -1,3 +1,26 @@
<script setup>
import { ref, watch } from 'vue'
import * as models from '../../../data/chat/models'
const props = defineProps({
imgUrl: String,
height: String,
width: String,
})
const showImgUrl = ref(props.imgUrl)
watch(() => props.imgUrl, (val) => {
showImgUrl.value = val
})
function onLoadError() {
if (showImgUrl.value !== models.DEFAULT_AVATAR_URL) {
showImgUrl.value = models.DEFAULT_AVATAR_URL
}
}
</script>
<template>
<yt-img-shadow
class="no-transition"
@@ -19,27 +42,4 @@
</yt-img-shadow>
</template>
<script setup>
import { ref, watch } from 'vue'
import * as models from '../../../data/chat/models'
const props = defineProps({
imgUrl: String,
height: String,
width: String
})
const showImgUrl = ref(props.imgUrl)
watch(() => props.imgUrl, (val) => {
showImgUrl.value = val
})
function onLoadError() {
if (showImgUrl.value !== models.DEFAULT_AVATAR_URL) {
showImgUrl.value = models.DEFAULT_AVATAR_URL
}
}
</script>
<style src="@/assets/css/youtube/yt-img-shadow.css"></style>

View File

@@ -1,3 +1,22 @@
<script setup>
import { computed } from 'vue'
import AuthorChip from './AuthorChip.vue'
import ImgShadow from './ImgShadow.vue'
import * as utils from './utils'
const props = defineProps({
avatarUrl: String,
authorName: String,
privilegeType: Number,
title: String,
time: Date,
})
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
</script>
<template>
<yt-live-chat-membership-item-renderer
class="style-scope yt-live-chat-item-list-renderer"
@@ -12,7 +31,7 @@
id="header"
class="style-scope yt-live-chat-membership-item-renderer"
>
<img-shadow
<ImgShadow
id="author-photo"
height="40"
width="40"
@@ -31,7 +50,7 @@
id="header-content-inner-column"
class="style-scope yt-live-chat-membership-item-renderer"
>
<author-chip
<AuthorChip
class="style-scope yt-live-chat-membership-item-renderer"
is-in-member-message
:author-name="authorName"
@@ -58,23 +77,4 @@
</yt-live-chat-membership-item-renderer>
</template>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import AuthorChip from './AuthorChip.vue'
import * as utils from './utils'
const props = defineProps({
avatarUrl: String,
authorName: String,
privilegeType: Number,
title: String,
time: Date
})
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
</script>
<style src="@/assets/css/youtube/yt-live-chat-membership-item-renderer.css"></style>

View File

@@ -1,97 +1,12 @@
<template>
<yt-live-chat-renderer
class="style-scope yt-live-chat-app"
style="--scrollbar-width:11px;"
hide-timestamps
@mousemove="refreshCantScrollStartTime"
>
<ticker
v-model:messages="paidMessages"
class="style-scope yt-live-chat-renderer"
:show-gift-name="showGiftName || undefined"
/>
<yt-live-chat-item-list-renderer
class="style-scope yt-live-chat-renderer"
allow-scroll
>
<div
id="item-scroller"
ref="scroller"
class="style-scope yt-live-chat-item-list-renderer animated"
@scroll="onScroll"
>
<div
id="item-offset"
ref="itemOffset"
class="style-scope yt-live-chat-item-list-renderer"
>
<div
id="items"
ref="items"
class="style-scope yt-live-chat-item-list-renderer"
style="overflow: hidden"
:style="{ transform: `translateY(${Math.floor(scrollPixelsRemaining)}px)` }"
>
<template
v-for="message in messages"
:key="message.id"
>
<text-message
v-if="message.type === MESSAGE_TYPE_TEXT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="message.authorName"
:author-type="message.authorType"
:privilege-type="message.privilegeType"
:content-parts="getShowContentParts(message)"
:repeated="message.repeated"
/>
<paid-message
v-else-if="message.type === MESSAGE_TYPE_GIFT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:price="message.price"
:price-text="message.price <= 0 ? getGiftShowNameAndNum(message) : ''"
:content="message.price <= 0 ? '' : getGiftShowContent(message, showGiftName)"
/>
<membership-item
v-else-if="message.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:privilege-type="message.privilegeType"
:title="message.title"
/>
<paid-message
v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:price="message.price"
:content="getShowContent(message)"
/>
</template>
</div>
</div>
</div>
</yt-live-chat-item-list-renderer>
</yt-live-chat-renderer>
</template>
<script>
import { useDebounceFn } from '@vueuse/core'
import _ from 'lodash'
import Ticker from './Ticker.vue'
import TextMessage from './TextMessage.vue'
import { defineComponent } from 'vue'
import * as constants from './constants'
import MembershipItem from './MembershipItem.vue'
import PaidMessage from './PaidMessage.vue'
import * as constants from './constants'
import { defineComponent } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import TextMessage from './TextMessage.vue'
import Ticker from './Ticker.vue'
// 要添加的消息类型
const ADD_MESSAGE_TYPES = [
@@ -116,24 +31,24 @@ export default defineComponent({
Ticker,
TextMessage,
MembershipItem,
PaidMessage
PaidMessage,
},
props: {
maxNumber: {
type: Number,
default: 60
default: 60,
},
showGiftName: {
type: Boolean,
default: false
default: false,
},
customCss: {
type: String,
default: ''
default: '',
},
},
data() {
let customStyleElement = document.createElement('style')
const customStyleElement = document.createElement('style')
document.head.appendChild(customStyleElement)
const setCssDebounce = useDebounceFn(() => {
customStyleElement.innerHTML = this.customCss ?? ''
@@ -145,27 +60,27 @@ export default defineComponent({
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
MESSAGE_TYPE_SUPER_CHAT: constants.MESSAGE_TYPE_SUPER_CHAT,
messages: [], // 显示的消息
paidMessages: [], // 固定在上方的消息
messages: [], // 显示的消息
paidMessages: [], // 固定在上方的消息
smoothedMessageQueue: [], // 平滑消息队列由外部调用addMessages等方法添加
emitSmoothedMessageTimerId: null, // 消费平滑消息队列的定时器ID
enqueueIntervals: [], // 最近进队列的时间间隔,用来估计下次进队列的时间
lastEnqueueTime: null, // 上次进队列的时间
estimatedEnqueueInterval: null, // 估计的下次进队列时间间隔
smoothedMessageQueue: [], // 平滑消息队列由外部调用addMessages等方法添加
emitSmoothedMessageTimerId: null, // 消费平滑消息队列的定时器ID
enqueueIntervals: [], // 最近进队列的时间间隔,用来估计下次进队列的时间
lastEnqueueTime: null, // 上次进队列的时间
estimatedEnqueueInterval: null, // 估计的下次进队列时间间隔
messagesBuffer: [], // 暂时未显示的消息,当不能自动滚动时会积压在这
preinsertHeight: 0, // 插入新消息之前items的高度
isSmoothed: true, // 是否平滑滚动,当消息太快时不平滑滚动
chatRateMs: 1000, // 用来计算消息速度
scrollPixelsRemaining: 0, // 平滑滚动剩余像素
scrollTimeRemainingMs: 0, // 平滑滚动剩余时间
lastSmoothChatMessageAddMs: null, // 上次showNewMessages时间
smoothScrollRafHandle: null, // 平滑滚动requestAnimationFrame句柄
lastSmoothScrollUpdate: null, // 平滑滚动上一帧时间
messagesBuffer: [], // 暂时未显示的消息,当不能自动滚动时会积压在这
preinsertHeight: 0, // 插入新消息之前items的高度
isSmoothed: true, // 是否平滑滚动,当消息太快时不平滑滚动
chatRateMs: 1000, // 用来计算消息速度
scrollPixelsRemaining: 0, // 平滑滚动剩余像素
scrollTimeRemainingMs: 0, // 平滑滚动剩余时间
lastSmoothChatMessageAddMs: null, // 上次showNewMessages时间
smoothScrollRafHandle: null, // 平滑滚动requestAnimationFrame句柄
lastSmoothScrollUpdate: null, // 平滑滚动上一帧时间
atBottom: true, // 滚动到底部,用来判断能否自动滚动
cantScrollStartTime: null, // 开始不能自动滚动的时间,用来防止卡住
atBottom: true, // 滚动到底部,用来判断能否自动滚动
cantScrollStartTime: null, // 开始不能自动滚动的时间,用来防止卡住
customStyleElement,
@@ -175,7 +90,7 @@ export default defineComponent({
computed: {
canScrollToBottom() {
return this.atBottom/* || this.allowScroll */
}
},
},
watch: {
canScrollToBottom(val) {
@@ -185,8 +100,8 @@ export default defineComponent({
immediate: true,
handler(val, oldVal) {
this.setCssDebounce(val)
}
}
},
},
},
mounted() {
this.scrollToBottom()
@@ -220,12 +135,12 @@ export default defineComponent({
// 后悔加这个功能了
mergeSimilarText(content) {
content = content.trim().toLowerCase()
for (let message of this.iterRecentMessages(5)) {
for (const message of this.iterRecentMessages(5)) {
if (message.type !== constants.MESSAGE_TYPE_TEXT) {
continue
}
let messageContent = message.content.trim().toLowerCase()
const messageContent = message.content.trim().toLowerCase()
let longer, shorter
if (messageContent.length > content.length) {
longer = messageContent
@@ -236,13 +151,13 @@ export default defineComponent({
}
if (
longer.indexOf(shorter) !== -1 // 长的包含短的
longer.includes(shorter) // 长的包含短的
&& longer.length - shorter.length < shorter.length // 长度差较小
) {
this.updateMessage(message.id, {
$add: {
repeated: 1
}
repeated: 1,
},
})
return true
}
@@ -250,7 +165,7 @@ export default defineComponent({
return false
},
mergeSimilarGift(authorName, price, _freePrice, giftName, num) {
for (let message of this.iterRecentMessages(5)) {
for (const message of this.iterRecentMessages(5)) {
if (
message.type === constants.MESSAGE_TYPE_GIFT
&& message.authorName === authorName
@@ -258,10 +173,10 @@ export default defineComponent({
) {
this.updateMessage(message.id, {
$add: {
price: price,
price,
// freePrice: freePrice, // 暂时没用到
num: num
}
num,
},
})
return true
}
@@ -269,13 +184,13 @@ export default defineComponent({
return false
},
// 从新到老迭代num条消息注意会迭代smoothedMessageQueue不会迭代paidMessages
*iterRecentMessages(num, onlyCountAddMessages = true) {
* iterRecentMessages(num, onlyCountAddMessages = true) {
if (num <= 0) {
return
}
for (let arr of this.iterMessageArrs()) {
for (const arr of this.iterMessageArrs()) {
for (let i = arr.length - 1; i >= 0 && num > 0; i--) {
let message = arr[i]
const message = arr[i]
yield message
if (!onlyCountAddMessages || this.isAddMessage(message)) {
num--
@@ -287,7 +202,7 @@ export default defineComponent({
}
},
// 从新到老迭代消息的数组
*iterMessageArrs() {
* iterMessageArrs() {
for (let i = this.smoothedMessageQueue.length - 1; i >= 0; i--) {
yield this.smoothedMessageQueue[i]
}
@@ -301,8 +216,8 @@ export default defineComponent({
this.enqueueMessages(ids.map(
id => ({
type: constants.MESSAGE_TYPE_DEL,
id
})
id,
}),
))
},
clearMessages() {
@@ -326,7 +241,7 @@ export default defineComponent({
this.enqueueMessages([{
type: constants.MESSAGE_TYPE_UPDATE,
id,
newValuesObj
newValuesObj,
}])
},
@@ -335,8 +250,8 @@ export default defineComponent({
if (!this.lastEnqueueTime) {
this.lastEnqueueTime = new Date()
} else {
let curTime = new Date()
let interval = curTime - this.lastEnqueueTime
const curTime = new Date()
const interval = curTime - this.lastEnqueueTime
// 真实的进队列时间间隔模式大概是这样2500, 300, 300, 300, 2500, 300, ...
// B站消息有缓冲会一次发多条消息。这里把波峰视为发送了一次真实的WS消息所以要过滤掉间隔太小的
if (interval > 1000 || this.enqueueIntervals.length < 5) {
@@ -354,7 +269,7 @@ export default defineComponent({
// 把messages分成messageGroup每个组里最多有1个需要平滑的消息
let messageGroup = []
for (let message of messages) {
for (const message of messages) {
messageGroup.push(message)
if (this.isAddMessage(message)) {
this.smoothedMessageQueue.push(messageGroup)
@@ -365,8 +280,8 @@ export default defineComponent({
if (messageGroup.length > 0) {
if (this.smoothedMessageQueue.length > 0) {
// 和上一组合并
let lastMessageGroup = this.smoothedMessageQueue[this.smoothedMessageQueue.length - 1]
for (let message of messageGroup) {
const lastMessageGroup = this.smoothedMessageQueue[this.smoothedMessageQueue.length - 1]
for (const message of messageGroup) {
lastMessageGroup.push(message)
}
} else {
@@ -380,7 +295,7 @@ export default defineComponent({
}
},
isAddMessage({ type }) {
return ADD_MESSAGE_TYPES.indexOf(type) !== -1
return ADD_MESSAGE_TYPES.includes(type)
},
emitSmoothedMessages() {
this.emitSmoothedMessageTimerId = null
@@ -395,9 +310,9 @@ export default defineComponent({
}
// 计算发送的消息数,保证在下次进队列之前发完
// 下次进队列之前应该发多少条消息
let shouldEmitGroupNum = Math.max(this.smoothedMessageQueue.length, 0)
const shouldEmitGroupNum = Math.max(this.smoothedMessageQueue.length, 0)
// 下次进队列之前最多能发多少次
let maxCanEmitCount = estimatedNextEnqueueRemainTime / MESSAGE_MIN_INTERVAL
const maxCanEmitCount = estimatedNextEnqueueRemainTime / MESSAGE_MIN_INTERVAL
// 这次发多少条消息
let groupNumToEmit
if (shouldEmitGroupNum < maxCanEmitCount) {
@@ -409,10 +324,10 @@ export default defineComponent({
}
// 发消息
let messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
let mergedGroup = []
for (let messageGroup of messageGroups) {
for (let message of messageGroup) {
const messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
const mergedGroup = []
for (const messageGroup of messageGroups) {
for (const message of messageGroup) {
mergedGroup.push(message)
}
}
@@ -444,7 +359,7 @@ export default defineComponent({
return
}
for (let message of messageGroup) {
for (const message of messageGroup) {
switch (message.type) {
case constants.MESSAGE_TYPE_TEXT:
case constants.MESSAGE_TYPE_GIFT:
@@ -482,9 +397,9 @@ export default defineComponent({
this.messagesBuffer.push(message)
},
handleDelMessage({ id }) {
let arrs = [this.messages, this.paidMessages, this.messagesBuffer]
const arrs = [this.messages, this.paidMessages, this.messagesBuffer]
let needResetSmoothScroll = false
for (let arr of arrs) {
for (const arr of arrs) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].id !== id) {
continue
@@ -501,10 +416,10 @@ export default defineComponent({
}
},
handleUpdateMessage({ id, newValuesObj }) {
let arrs = [this.messages, this.paidMessages, this.messagesBuffer]
const arrs = [this.messages, this.paidMessages, this.messagesBuffer]
let needResetSmoothScroll = false
for (let arr of arrs) {
for (let message of arr) {
for (const arr of arrs) {
for (const message of arr) {
if (message.id !== id) {
continue
}
@@ -521,15 +436,15 @@ export default defineComponent({
},
doUpdateMessage(message, newValuesObj) {
// +=
let addValuesObj = newValuesObj.$add
const addValuesObj = newValuesObj.$add
if (addValuesObj !== undefined) {
for (let name in addValuesObj) {
for (const name in addValuesObj) {
message[name] += addValuesObj[name]
}
}
// =
for (let name in newValuesObj) {
for (const name in newValuesObj) {
if (!name.startsWith('$')) {
message[name] = newValuesObj[name]
}
@@ -548,7 +463,7 @@ export default defineComponent({
return
}
let removeNum = Math.max(this.messages.length + this.messagesBuffer.length - this.maxNumber, 0)
const removeNum = Math.max(this.messages.length + this.messagesBuffer.length - this.maxNumber, 0)
if (removeNum > 0) {
this.messages.splice(0, removeNum)
// 防止同时添加和删除项目时所有的项目重新渲染 https://github.com/vuejs/vue/issues/6857
@@ -556,7 +471,7 @@ export default defineComponent({
}
this.preinsertHeight = this.$refs.items.clientHeight
for (let message of this.messagesBuffer) {
for (const message of this.messagesBuffer) {
this.messages.push(message)
}
this.messagesBuffer = []
@@ -565,7 +480,7 @@ export default defineComponent({
this.showNewMessages()
},
showNewMessages() {
let hasScrollBar = this.$refs.items.clientHeight > this.$refs.scroller.clientHeight
const hasScrollBar = this.$refs.items.clientHeight > this.$refs.scroller.clientHeight
this.$refs.itemOffset.style.height = `${this.$refs.items.clientHeight}px`
if (!this.canScrollToBottomOrTimedOut() || !hasScrollBar) {
return
@@ -579,7 +494,7 @@ export default defineComponent({
if (!this.lastSmoothChatMessageAddMs) {
this.lastSmoothChatMessageAddMs = performance.now()
}
let interval = performance.now() - this.lastSmoothChatMessageAddMs
const interval = performance.now() - this.lastSmoothChatMessageAddMs
this.chatRateMs = (0.9 * this.chatRateMs) + (0.1 * interval)
if (this.isSmoothed) {
if (this.chatRateMs < 400) {
@@ -605,9 +520,9 @@ export default defineComponent({
return
}
let interval = time - this.lastSmoothScrollUpdate
const interval = time - this.lastSmoothScrollUpdate
if (
this.scrollPixelsRemaining <= 0 || this.scrollPixelsRemaining >= 400 // 已经滚动到底部或者离底部太远则结束
this.scrollPixelsRemaining <= 0 || this.scrollPixelsRemaining >= 400 // 已经滚动到底部或者离底部太远则结束
|| interval >= 1000 // 离上一帧时间太久,可能用户切换到其他网页
|| this.scrollTimeRemainingMs <= 0 // 时间已结束
) {
@@ -615,7 +530,7 @@ export default defineComponent({
return
}
let pixelsToScroll = interval / this.scrollTimeRemainingMs * this.scrollPixelsRemaining
const pixelsToScroll = interval / this.scrollTimeRemainingMs * this.scrollPixelsRemaining
this.scrollPixelsRemaining -= pixelsToScroll
if (this.scrollPixelsRemaining < 0) {
this.scrollPixelsRemaining = 0
@@ -647,12 +562,12 @@ export default defineComponent({
}
},
scrollToBottom() {
this.$refs.scroller.scrollTop = Math.pow(2, 24)
this.$refs.scroller.scrollTop = 2 ** 24
this.atBottom = true
},
onScroll() {
this.refreshCantScrollStartTime()
let scroller = this.$refs.scroller
const scroller = this.$refs.scroller
this.atBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < SCROLLED_TO_BOTTOM_EPSILON
this.flushMessagesBuffer()
},
@@ -668,11 +583,98 @@ export default defineComponent({
if (this.cantScrollStartTime) {
this.cantScrollStartTime = new Date()
}
}
}
},
},
})
</script>
<template>
<yt-live-chat-renderer
class="style-scope yt-live-chat-app"
style="--scrollbar-width:11px;"
hide-timestamps
@mousemove="refreshCantScrollStartTime"
>
<Ticker
v-model:messages="paidMessages"
class="style-scope yt-live-chat-renderer"
:show-gift-name="showGiftName || undefined"
/>
<yt-live-chat-item-list-renderer
class="style-scope yt-live-chat-renderer"
allow-scroll
>
<div
id="item-scroller"
ref="scroller"
class="style-scope yt-live-chat-item-list-renderer animated"
@scroll="onScroll"
>
<div
id="item-offset"
ref="itemOffset"
class="style-scope yt-live-chat-item-list-renderer"
>
<div
id="items"
ref="items"
class="style-scope yt-live-chat-item-list-renderer"
style="overflow: hidden"
:style="{ transform: `translateY(${Math.floor(scrollPixelsRemaining)}px)` }"
>
<template
v-for="message in messages"
:key="message.id"
>
<TextMessage
v-if="message.type === MESSAGE_TYPE_TEXT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="message.authorName"
:author-type="message.authorType"
:privilege-type="message.privilegeType"
:content-parts="getShowContentParts(message)"
:repeated="message.repeated"
/>
<PaidMessage
v-else-if="message.type === MESSAGE_TYPE_GIFT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:price="message.price"
:price-text="message.price <= 0 ? getGiftShowNameAndNum(message) : ''"
:content="message.price <= 0 ? '' : getGiftShowContent(message, showGiftName)"
/>
<MembershipItem
v-else-if="message.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:privilege-type="message.privilegeType"
:title="message.title"
/>
<PaidMessage
v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
class="style-scope yt-live-chat-item-list-renderer"
:time="message.time"
:avatar-url="message.avatarUrl"
:author-name="getShowAuthorName(message)"
:price="message.price"
:content="getShowContent(message)"
/>
</template>
</div>
</div>
</div>
</yt-live-chat-item-list-renderer>
</yt-live-chat-renderer>
</template>
<style src="@/assets/css/youtube/yt-html.css"></style>
<style src="@/assets/css/youtube/yt-live-chat-renderer.css"></style>
<style src="@/assets/css/youtube/yt-live-chat-item-list-renderer.css"></style>
<style src="@/assets/css/youtube/yt-live-chat-item-list-renderer.css"></style>

View File

@@ -1,3 +1,35 @@
<script setup>
import { computed } from 'vue'
import * as constants from './constants'
import ImgShadow from './ImgShadow.vue'
import * as utils from './utils'
const props = defineProps({
avatarUrl: String,
authorName: String,
price: Number, // 价格,人民币
priceText: String,
time: Date,
content: String,
})
const priceConfig = computed(() => {
return constants.getPriceConfig(props.price)
})
const color = computed(() => {
return priceConfig.value.colors
})
const showPriceText = computed(() => {
return props.priceText || `CN¥${utils.formatCurrency(props.price)}`
})
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
</script>
<template>
<yt-live-chat-paid-message-renderer
class="style-scope yt-live-chat-item-list-renderer"
@@ -9,7 +41,7 @@
'--yt-live-chat-paid-message-header-color': color.header,
'--yt-live-chat-paid-message-author-name-color': color.authorName,
'--yt-live-chat-paid-message-timestamp-color': color.time,
'--yt-live-chat-paid-message-color': color.content
'--yt-live-chat-paid-message-color': color.content,
}"
:blc-price-level="priceConfig.priceLevel"
>
@@ -21,7 +53,7 @@
id="header"
class="style-scope yt-live-chat-paid-message-renderer"
>
<img-shadow
<ImgShadow
id="author-photo"
height="40"
width="40"
@@ -71,36 +103,4 @@
</yt-live-chat-paid-message-renderer>
</template>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import * as constants from './constants'
import * as utils from './utils'
const props = defineProps({
avatarUrl: String,
authorName: String,
price: Number, // 价格,人民币
priceText: String,
time: Date,
content: String
})
const priceConfig = computed(() => {
return constants.getPriceConfig(props.price)
})
const color = computed(() => {
return priceConfig.value.colors
})
const showPriceText = computed(() => {
return props.priceText || `CN¥${utils.formatCurrency(props.price)}`
})
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
</script>
<style src="@/assets/css/youtube/yt-live-chat-paid-message-renderer.css"></style>

View File

@@ -1,9 +1,58 @@
<script setup>
import { NBadge } from 'naive-ui'
import { computed } from 'vue'
import AuthorChip from './AuthorChip.vue'
import * as constants from './constants'
import ImgShadow from './ImgShadow.vue'
import * as utils from './utils'
const props = defineProps({
avatarUrl: String,
time: Date,
authorName: String,
authorType: Number,
contentParts: Array,
privilegeType: Number,
repeated: Number,
})
// HSL
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
const CONTENT_TYPE_TEXT = constants.CONTENT_TYPE_TEXT
const CONTENT_TYPE_IMAGE = constants.CONTENT_TYPE_IMAGE
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
const authorTypeText = computed(() => {
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
})
const repeatedMarkColor = computed(() => {
let color
if (props.repeated <= 2) {
color = REPEATED_MARK_COLOR_START
} else if (props.repeated >= 10) {
color = REPEATED_MARK_COLOR_END
} else {
color = [0, 0, 0]
const t = (props.repeated - 2) / (10 - 2)
for (let i = 0; i < 3; i++) {
color[i] = REPEATED_MARK_COLOR_START[i] + ((REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t)
}
}
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
})
</script>
<template>
<yt-live-chat-text-message-renderer
:author-type="authorTypeText"
:blc-guard-level="privilegeType"
>
<img-shadow
<ImgShadow
id="author-photo"
height="24"
width="24"
@@ -18,7 +67,7 @@
id="timestamp"
class="style-scope yt-live-chat-text-message-renderer"
>{{ timeText }}</span>
<author-chip
<AuthorChip
class="style-scope yt-live-chat-text-message-renderer"
:is-in-member-message="false"
:author-name="authorName"
@@ -38,7 +87,7 @@
<img
v-else-if="content.type === CONTENT_TYPE_IMAGE"
:id="`emoji-${content.text}`"
:key="'_' + index"
:key="`_${index}`"
class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
:src="content.url"
:alt="content.text"
@@ -61,56 +110,6 @@
</yt-live-chat-text-message-renderer>
</template>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import AuthorChip from './AuthorChip.vue'
import * as constants from './constants'
import * as utils from './utils'
import { NBadge } from 'naive-ui'
// HSL
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
const CONTENT_TYPE_TEXT = constants.CONTENT_TYPE_TEXT
const CONTENT_TYPE_IMAGE = constants.CONTENT_TYPE_IMAGE
const props = defineProps({
avatarUrl: String,
time: Date,
authorName: String,
authorType: Number,
contentParts: Array,
privilegeType: Number,
repeated: Number
})
const timeText = computed(() => {
return utils.getTimeTextHourMin(props.time)
})
const authorTypeText = computed(() => {
return constants.AUTHOR_TYPE_TO_TEXT[props.authorType]
})
const repeatedMarkColor = computed(() => {
let color
if (props.repeated <= 2) {
color = REPEATED_MARK_COLOR_START
} else if (props.repeated >= 10) {
color = REPEATED_MARK_COLOR_END
} else {
color = [0, 0, 0]
let t = (props.repeated - 2) / (10 - 2)
for (let i = 0; i < 3; i++) {
color[i] = REPEATED_MARK_COLOR_START[i] + ((REPEATED_MARK_COLOR_END[i] - REPEATED_MARK_COLOR_START[i]) * t)
}
}
return `hsl(${color[0]}, ${color[1]}%, ${color[2]}%)`
})
</script>
<style>
yt-live-chat-text-message-renderer>#content>#message>.el-badge {
margin-left: 10px;

View File

@@ -1,99 +1,19 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
<div
id="container"
dir="ltr"
class="style-scope yt-live-chat-ticker-renderer"
>
<transition-group
id="items"
tag="div"
:css="false"
class="style-scope yt-live-chat-ticker-renderer"
@enter="onTickerItemEnter"
@leave="onTickerItemLeave"
>
<yt-live-chat-ticker-paid-message-item-renderer
v-for="message in showMessages"
:key="message.raw.id"
tabindex="0"
class="style-scope yt-live-chat-ticker-renderer"
style="overflow: hidden;"
@click="onItemClick(message.raw)"
>
<div
id="container"
dir="ltr"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:style="{
background: message.bgColor,
}"
>
<div
id="content"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:style="{
color: message.color
}"
>
<img-shadow
id="author-photo"
height="24"
width="24"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:img-url="message.raw.avatarUrl"
/>
<span
id="text"
dir="ltr"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
>{{ message.text }}</span>
</div>
</div>
</yt-live-chat-ticker-paid-message-item-renderer>
</transition-group>
</div>
<template v-if="pinnedMessage">
<membership-item
v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
:key="pinnedMessage.id"
class="style-scope yt-live-chat-ticker-renderer"
:avatar-url="pinnedMessage.avatarUrl"
:author-name="getShowAuthorName(pinnedMessage)"
:privilege-type="pinnedMessage.privilegeType"
:title="pinnedMessage.title"
:time="pinnedMessage.time"
/>
<paid-message
v-else
:key="pinnedMessage.id"
class="style-scope yt-live-chat-ticker-renderer"
:price="pinnedMessage.price"
:avatar-url="pinnedMessage.avatarUrl"
:author-name="getShowAuthorName(pinnedMessage)"
:time="pinnedMessage.time"
:content="pinnedMessageShowContent"
/>
</template>
</yt-live-chat-ticker-renderer>
</template>
<script setup>
// @ts-nocheck
import { ref, computed, onBeforeUnmount, nextTick } from 'vue'
import { formatCurrency } from './utils'
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import * as constants from './constants'
import ImgShadow from './ImgShadow.vue'
import MembershipItem from './MembershipItem.vue'
import PaidMessage from './PaidMessage.vue'
import * as constants from './constants'
import { formatCurrency } from './utils'
const props = defineProps({
messages: Array,
showGiftName: {
type: Boolean,
default: false
}
default: false,
},
})
const emit = defineEmits(['update:messages'])
@@ -109,8 +29,8 @@ onBeforeUnmount(() => {
})
const showMessages = computed(() => {
let res = []
for (let message of props.messages) {
const res = []
for (const message of props.messages) {
if (!needToShow(message)) {
continue
}
@@ -118,7 +38,7 @@ const showMessages = computed(() => {
raw: message,
bgColor: getBgColor(message),
color: getColor(message),
text: getText(message)
text: getText(message),
})
}
return res
@@ -136,7 +56,7 @@ const pinnedMessageShowContent = computed(() => {
})
async function onTickerItemEnter(el, done) {
let width = el.clientWidth
const width = el.clientWidth
if (width === 0) {
// CSS指定了不显示固定栏
done()
@@ -165,7 +85,7 @@ function onTickerItemLeave(el, done) {
const getShowAuthorName = constants.getShowAuthorName
function needToShow(message) {
let pinTime = getPinTime(message)
const pinTime = getPinTime(message)
return (new Date() - message.addTime) / (60 * 1000) < pinTime
}
@@ -175,11 +95,11 @@ function getBgColor(message) {
color1 = 'rgba(15,157,88,1)'
color2 = 'rgba(11,128,67,1)'
} else {
let config = constants.getPriceConfig(message.price)
const config = constants.getPriceConfig(message.price)
color1 = config.colors.contentBg
color2 = config.colors.headerBg
}
let pinTime = getPinTime(message)
const pinTime = getPinTime(message)
let progress = (1 - ((curTime.value - message.addTime) / (60 * 1000) / pinTime)) * 100
if (progress < 0) {
progress = 0
@@ -215,10 +135,10 @@ function updateProgress() {
curTime.value = new Date()
// 删除过期的消息
let filteredMessages = []
const filteredMessages = []
let messagesChanged = false
for (let message of props.messages) {
let pinTime = getPinTime(message)
for (const message of props.messages) {
const pinTime = getPinTime(message)
if ((curTime.value - message.addTime) / (60 * 1000) >= pinTime) {
messagesChanged = true
if (pinnedMessage.value === message) {
@@ -242,5 +162,86 @@ function onItemClick(message) {
}
</script>
<template>
<yt-live-chat-ticker-renderer :hidden="showMessages.length === 0">
<div
id="container"
dir="ltr"
class="style-scope yt-live-chat-ticker-renderer"
>
<transition-group
id="items"
tag="div"
:css="false"
class="style-scope yt-live-chat-ticker-renderer"
@enter="onTickerItemEnter"
@leave="onTickerItemLeave"
>
<yt-live-chat-ticker-paid-message-item-renderer
v-for="message in showMessages"
:key="message.raw.id"
tabindex="0"
class="style-scope yt-live-chat-ticker-renderer"
style="overflow: hidden;"
@click="onItemClick(message.raw)"
>
<div
id="container"
dir="ltr"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:style="{
background: message.bgColor,
}"
>
<div
id="content"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:style="{
color: message.color,
}"
>
<ImgShadow
id="author-photo"
height="24"
width="24"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
:img-url="message.raw.avatarUrl"
/>
<span
id="text"
dir="ltr"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer"
>{{ message.text }}</span>
</div>
</div>
</yt-live-chat-ticker-paid-message-item-renderer>
</transition-group>
</div>
<template v-if="pinnedMessage">
<MembershipItem
v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
:key="pinnedMessage.id"
class="style-scope yt-live-chat-ticker-renderer"
:avatar-url="pinnedMessage.avatarUrl"
:author-name="getShowAuthorName(pinnedMessage)"
:privilege-type="pinnedMessage.privilegeType"
:title="pinnedMessage.title"
:time="pinnedMessage.time"
/>
<PaidMessage
v-else
:key="pinnedMessage.id"
class="style-scope yt-live-chat-ticker-renderer"
:price="pinnedMessage.price"
:avatar-url="pinnedMessage.avatarUrl"
:author-name="getShowAuthorName(pinnedMessage)"
:time="pinnedMessage.time"
:content="pinnedMessageShowContent"
/>
</template>
</yt-live-chat-ticker-renderer>
</template>
<style src="@/assets/css/youtube/yt-live-chat-ticker-renderer.css"></style>
<style src="@/assets/css/youtube/yt-live-chat-ticker-paid-message-item-renderer.css"></style>

View File

@@ -7,7 +7,7 @@ export const AUTHOR_TYPE_TO_TEXT = [
'',
'member', // 舰队
'moderator', // 房管
'owner' // 主播
'owner', // 主播
]
export function getShowGuardLevelText(guardLevel) {
@@ -45,7 +45,7 @@ const PRICE_CONFIGS = [
header: 'rgba(0,0,0,1)',
authorName: 'rgba(0,0,0,0.701961)',
time: 'rgba(0,0,0,0.501961)',
content: 'rgba(0,0,0,1)'
content: 'rgba(0,0,0,1)',
},
pinTime: 0,
priceLevel: 0,
@@ -59,7 +59,7 @@ const PRICE_CONFIGS = [
header: 'rgba(255,255,255,1)',
authorName: 'rgba(255,255,255,0.701961)',
time: 'rgba(255,255,255,0.501961)',
content: 'rgba(255,255,255,1)'
content: 'rgba(255,255,255,1)',
},
pinTime: 0,
priceLevel: 1,
@@ -73,7 +73,7 @@ const PRICE_CONFIGS = [
header: 'rgba(0,0,0,1)',
authorName: 'rgba(0,0,0,0.701961)',
time: 'rgba(0,0,0,0.501961)',
content: 'rgba(0,0,0,1)'
content: 'rgba(0,0,0,1)',
},
pinTime: 0,
priceLevel: 2,
@@ -87,7 +87,7 @@ const PRICE_CONFIGS = [
header: 'rgba(0,0,0,1)',
authorName: 'rgba(0,0,0,0.541176)',
time: 'rgba(0,0,0,0.501961)',
content: 'rgba(0,0,0,1)'
content: 'rgba(0,0,0,1)',
},
pinTime: 2,
priceLevel: 3,
@@ -101,7 +101,7 @@ const PRICE_CONFIGS = [
header: 'rgba(0,0,0,0.87451)',
authorName: 'rgba(0,0,0,0.541176)',
time: 'rgba(0,0,0,0.501961)',
content: 'rgba(0,0,0,0.87451)'
content: 'rgba(0,0,0,0.87451)',
},
pinTime: 5,
priceLevel: 4,
@@ -115,7 +115,7 @@ const PRICE_CONFIGS = [
header: 'rgba(255,255,255,0.87451)',
authorName: 'rgba(255,255,255,0.701961)',
time: 'rgba(255,255,255,0.501961)',
content: 'rgba(255,255,255,0.87451)'
content: 'rgba(255,255,255,0.87451)',
},
pinTime: 10,
priceLevel: 5,
@@ -129,7 +129,7 @@ const PRICE_CONFIGS = [
header: 'rgba(255,255,255,1)',
authorName: 'rgba(255,255,255,0.701961)',
time: 'rgba(255,255,255,0.501961)',
content: 'rgba(255,255,255,1)'
content: 'rgba(255,255,255,1)',
},
pinTime: 30,
priceLevel: 6,
@@ -143,7 +143,7 @@ const PRICE_CONFIGS = [
header: 'rgba(255,255,255,1)',
authorName: 'rgba(255,255,255,0.701961)',
time: 'rgba(255,255,255,0.501961)',
content: 'rgba(255,255,255,1)'
content: 'rgba(255,255,255,1)',
},
pinTime: 60,
priceLevel: 7,
@@ -154,7 +154,7 @@ export function getPriceConfig(price) {
let i = 0
// 根据先验知识,从小找到大通常更快结束
for (; i < PRICE_CONFIGS.length - 1; i++) {
let nextConfig = PRICE_CONFIGS[i + 1]
const nextConfig = PRICE_CONFIGS[i + 1]
if (price < nextConfig.price) {
return PRICE_CONFIGS[i]
}
@@ -170,22 +170,22 @@ export function getShowContent(message) {
}
export function getShowRichContent(message) {
let richContent = [...message.richContent]
const richContent = [...message.richContent]
if (message.translation) {
richContent.push({
type: CONTENT_TYPE_TEXT,
text: `${message.translation}`
text: `${message.translation}`,
})
}
return richContent
}
export function getShowContentParts(message) {
let contentParts = [...message.contentParts || []]
const contentParts = [...message.contentParts || []]
if (message.translation) {
contentParts.push({
type: CONTENT_TYPE_TEXT,
text: `${message.translation}`
text: `${message.translation}`,
})
}
return contentParts

View File

@@ -1,9 +1,8 @@
import { format } from 'date-fns'
export function mergeConfig(config, defaultConfig) {
let res = {}
for (let i in defaultConfig) {
const res = {}
for (const i in defaultConfig) {
res[i] = i in config ? config[i] : defaultConfig[i]
}
return res
@@ -11,13 +10,13 @@ export function mergeConfig(config, defaultConfig) {
export function toBool(val) {
if (typeof val === 'string') {
return ['false', 'no', 'off', '0', ''].indexOf(val.toLowerCase()) === -1
return !['false', 'no', 'off', '0', ''].includes(val.toLowerCase())
}
return Boolean(val)
}
export function toInt(val, _default) {
let res = parseInt(val)
let res = Number.parseInt(val)
if (isNaN(res)) {
res = _default
}
@@ -25,7 +24,7 @@ export function toInt(val, _default) {
}
export function toFloat(val, _default) {
let res = parseFloat(val)
let res = Number.parseFloat(val)
if (isNaN(res)) {
res = _default
}
@@ -34,7 +33,7 @@ export function toFloat(val, _default) {
export function formatCurrency(price) {
return new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: price < 100 ? 2 : 0
minimumFractionDigits: price < 100 ? 2 : 0,
}).format(price)
}
@@ -43,9 +42,9 @@ export function getTimeTextHourMin(date) {
}
export function getUuid4Hex() {
let chars = []
const chars = []
for (let i = 0; i < 32; i++) {
let char = Math.floor(Math.random() * 16).toString(16)
const char = Math.floor(Math.random() * 16).toString(16)
chars.push(char)
}
return chars.join('')

View File

@@ -9,19 +9,19 @@ export class PronunciationConverter {
async loadDict(dictName) {
let promise
switch (dictName) {
case DICT_PINYIN:
promise = import('./dictPinyin')
break
case DICT_KANA:
promise = import('./dictKana')
break
default:
return
case DICT_PINYIN:
promise = import('./dictPinyin')
break
case DICT_KANA:
promise = import('./dictKana')
break
default:
return
}
let dictTxt = (await promise).default
let pronunciationMap = new Map()
for (let item of dictTxt.split('\n')) {
const dictTxt = (await promise).default
const pronunciationMap = new Map()
for (const item of dictTxt.split('\n')) {
if (item.length === 0) {
continue
}
@@ -31,10 +31,10 @@ export class PronunciationConverter {
}
getPronunciation(text) {
let res = []
const res = []
let lastHasPronunciation = null
for (let char of text) {
let pronunciation = this.pronunciationMap.get(char)
for (const char of text) {
const pronunciation = this.pronunciationMap.get(char)
if (pronunciation === undefined) {
if (lastHasPronunciation !== null && lastHasPronunciation) {
res.push(' ')

View File

@@ -6,7 +6,7 @@ export class Trie {
_createNode() {
return {
children: {}, // char -> node
value: null
value: null,
}
}
@@ -15,7 +15,7 @@ export class Trie {
throw new Error('key is empty')
}
let node = this._root
for (let char of key) {
for (const char of key) {
let nextNode = node.children[char]
if (nextNode === undefined) {
nextNode = node.children[char] = this._createNode()
@@ -27,8 +27,8 @@ export class Trie {
get(key) {
let node = this._root
for (let char of key) {
let nextNode = node.children[char]
for (const char of key) {
const nextNode = node.children[char]
if (nextNode === undefined) {
return null
}
@@ -43,8 +43,8 @@ export class Trie {
lazyMatch(str) {
let node = this._root
for (let char of str) {
let nextNode = node.children[char]
for (const char of str) {
const nextNode = node.children[char]
if (nextNode === undefined) {
return null
}