feat: 更新项目配置和依赖,增强功能和用户体验

- 完成弹幕机功能
- 在 .editorconfig 中新增对 vine.ts 文件的支持。
- 更新 package.json 中多个依赖的版本,提升稳定性和性能。
- 在 vite.config.mts 中引入 @guolao/vue-monaco-editor 插件,增强代码编辑功能。
- 在 App.vue 中调整内容填充的样式,优化界面布局。
- 新增获取配置文件哈希的 API 方法,提升配置管理能力。
- 在多个组件中优化了样式和逻辑,提升用户交互体验。
This commit is contained in:
2025-04-25 00:08:06 +08:00
parent b24974540f
commit 07948e6777
36 changed files with 3108 additions and 1258 deletions

View File

@@ -22,38 +22,34 @@
</yt-live-chat-author-badge-renderer>
</template>
<script>
import { NTooltip } from 'naive-ui';
<script setup>
import { computed } from 'vue'
import { NTooltip } from 'naive-ui'
import * as constants from './constants'
import { FILE_BASE_URL } from '@/data/constants';
import { FILE_BASE_URL } from '@/data/constants'
export default {
name: 'AuthorBadge',
props: {
isAdmin: Boolean,
privilegeType: Number
},
components: {
NTooltip
},
computed: {
authorTypeText() {
if (this.isAdmin) {
return 'moderator'
}
return this.privilegeType > 0 ? 'member' : ''
},
readableAuthorTypeText() {
if (this.isAdmin) {
return '管理员'
}
return constants.getShowGuardLevelText(this.privilegeType)
},
fileServerUrl() {
return FILE_BASE_URL
}
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>

View File

@@ -1,50 +1,63 @@
<template>
<yt-live-chat-author-chip>
<span id="author-name" dir="auto" class="style-scope yt-live-chat-author-chip"
:class="{ member: isInMemberMessage }" :type="authorTypeText">
<span
id="author-name"
dir="auto"
class="style-scope yt-live-chat-author-chip"
:class="{ member: isInMemberMessage }"
:type="authorTypeText"
>
{{ authorName }}
<!-- 这里是已验证勋章 -->
<span id="chip-badges" class="style-scope yt-live-chat-author-chip"></span>
<span
id="chip-badges"
class="style-scope yt-live-chat-author-chip"
/>
</span>
<span id="chat-badges" class="style-scope yt-live-chat-author-chip">
<author-badge v-if="isInMemberMessage" class="style-scope yt-live-chat-author-chip" :isAdmin="false"
:privilegeType="privilegeType"></author-badge>
<span
id="chat-badges"
class="style-scope yt-live-chat-author-chip"
>
<author-badge
v-if="isInMemberMessage"
class="style-scope yt-live-chat-author-chip"
:is-admin="false"
:privilege-type="privilegeType"
/>
<template v-else>
<author-badge v-if="authorType === AUTHOR_TYPE_ADMIN" class="style-scope yt-live-chat-author-chip" isAdmin
:privilegeType="0"></author-badge>
<author-badge v-if="privilegeType > 0" class="style-scope yt-live-chat-author-chip" :isAdmin="false"
:privilegeType="privilegeType"></author-badge>
<author-badge
v-if="authorType === AUTHOR_TYPE_ADMIN"
class="style-scope yt-live-chat-author-chip"
is-admin
:privilege-type="0"
/>
<author-badge
v-if="privilegeType > 0"
class="style-scope yt-live-chat-author-chip"
:is-admin="false"
:privilege-type="privilegeType"
/>
</template>
</span>
</yt-live-chat-author-chip>
</template>
<script>
import { defineComponent } from 'vue';
<script setup>
import { computed } from 'vue'
import AuthorBadge from './AuthorBadge.vue'
import * as constants from './constants'
export default defineComponent({
name: 'AuthorChip',
components: {
AuthorBadge
},
props: {
isInMemberMessage: Boolean,
authorName: String,
authorType: Number,
privilegeType: Number
},
data() {
return {
AUTHOR_TYPE_ADMIN: constants.AUTHOR_TYPE_ADMIN
}
},
computed: {
authorTypeText() {
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
}
}
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>

View File

@@ -1,36 +1,43 @@
<template>
<yt-img-shadow class="no-transition" :height="height" :width="width" style="background-color: transparent;" loaded>
<img id="img" class="style-scope yt-img-shadow" alt="" :height="height" :width="width" :src="showImgUrl"
@error="onLoadError" referrerpolicy="no-referrer">
<yt-img-shadow
class="no-transition"
:height="height"
:width="width"
style="background-color: transparent;"
loaded
>
<img
id="img"
class="style-scope yt-img-shadow"
alt=""
:height="height"
:width="width"
:src="showImgUrl"
referrerpolicy="no-referrer"
@error="onLoadError"
>
</yt-img-shadow>
</template>
<script>
<script setup>
import { ref, watch } from 'vue'
import * as models from '../../../data/chat/models'
export default {
name: 'ImgShadow',
props: {
imgUrl: String,
height: String,
width: String
},
data() {
return {
showImgUrl: this.imgUrl
}
},
watch: {
imgUrl(val) {
this.showImgUrl = val
}
},
methods: {
onLoadError() {
if (this.showImgUrl !== models.DEFAULT_AVATAR_URL) {
this.showImgUrl = models.DEFAULT_AVATAR_URL
}
}
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>

View File

@@ -1,52 +1,80 @@
<template>
<yt-live-chat-membership-item-renderer class="style-scope yt-live-chat-item-list-renderer" show-only-header
<yt-live-chat-membership-item-renderer
class="style-scope yt-live-chat-item-list-renderer"
show-only-header
:blc-guard-level="privilegeType"
>
<div id="card" class="style-scope yt-live-chat-membership-item-renderer">
<div id="header" class="style-scope yt-live-chat-membership-item-renderer">
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-membership-item-renderer"
:imgUrl="avatarUrl"
></img-shadow>
<div id="header-content" class="style-scope yt-live-chat-membership-item-renderer">
<div id="header-content-primary-column" class="style-scope yt-live-chat-membership-item-renderer">
<div id="header-content-inner-column" class="style-scope yt-live-chat-membership-item-renderer">
<author-chip class="style-scope yt-live-chat-membership-item-renderer"
isInMemberMessage :authorName="authorName" :authorType="0" :privilegeType="privilegeType"
></author-chip>
<div
id="card"
class="style-scope yt-live-chat-membership-item-renderer"
>
<div
id="header"
class="style-scope yt-live-chat-membership-item-renderer"
>
<img-shadow
id="author-photo"
height="40"
width="40"
class="style-scope yt-live-chat-membership-item-renderer"
:img-url="avatarUrl"
/>
<div
id="header-content"
class="style-scope yt-live-chat-membership-item-renderer"
>
<div
id="header-content-primary-column"
class="style-scope yt-live-chat-membership-item-renderer"
>
<div
id="header-content-inner-column"
class="style-scope yt-live-chat-membership-item-renderer"
>
<author-chip
class="style-scope yt-live-chat-membership-item-renderer"
is-in-member-message
:author-name="authorName"
:author-type="0"
:privilege-type="privilegeType"
/>
</div>
<div
id="header-subtext"
class="style-scope yt-live-chat-membership-item-renderer"
>
{{ title }}
</div>
<div id="header-subtext" class="style-scope yt-live-chat-membership-item-renderer">{{ title }}</div>
</div>
<div id="timestamp" class="style-scope yt-live-chat-membership-item-renderer">{{ timeText }}</div>
<div
id="timestamp"
class="style-scope yt-live-chat-membership-item-renderer"
>
{{ timeText }}
</div>
</div>
</div>
</div>
</yt-live-chat-membership-item-renderer>
</template>
<script>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import AuthorChip from './AuthorChip.vue'
import * as utils from './utils'
export default {
name: 'MembershipItem',
components: {
ImgShadow,
AuthorChip
},
props: {
avatarUrl: String,
authorName: String,
privilegeType: Number,
title: String,
time: Date
},
computed: {
timeText() {
return utils.getTimeTextHourMin(this.time)
}
}
}
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,6 +1,9 @@
<template>
<yt-live-chat-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer" allow-animations
:show-only-header="!content || undefined" :style="{
<yt-live-chat-paid-message-renderer
class="style-scope yt-live-chat-item-list-renderer"
allow-animations
:show-only-header="!content || undefined"
:style="{
'--yt-live-chat-paid-message-primary-color': color.contentBg,
'--yt-live-chat-paid-message-secondary-color': color.headerBg,
'--yt-live-chat-paid-message-header-color': color.header,
@@ -10,59 +13,94 @@
}"
:blc-price-level="priceConfig.priceLevel"
>
<div id="card" class="style-scope yt-live-chat-paid-message-renderer">
<div id="header" class="style-scope yt-live-chat-paid-message-renderer">
<img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-paid-message-renderer"
:imgUrl="avatarUrl"
></img-shadow>
<div id="header-content" class="style-scope yt-live-chat-paid-message-renderer">
<div id="header-content-primary-column" class="style-scope yt-live-chat-paid-message-renderer">
<div id="author-name" class="style-scope yt-live-chat-paid-message-renderer">{{ authorName }}</div>
<div id="purchase-amount" class="style-scope yt-live-chat-paid-message-renderer">{{ showPriceText }}</div>
<div
id="card"
class="style-scope yt-live-chat-paid-message-renderer"
>
<div
id="header"
class="style-scope yt-live-chat-paid-message-renderer"
>
<img-shadow
id="author-photo"
height="40"
width="40"
class="style-scope yt-live-chat-paid-message-renderer"
:img-url="avatarUrl"
/>
<div
id="header-content"
class="style-scope yt-live-chat-paid-message-renderer"
>
<div
id="header-content-primary-column"
class="style-scope yt-live-chat-paid-message-renderer"
>
<div
id="author-name"
class="style-scope yt-live-chat-paid-message-renderer"
>
{{ authorName }}
</div>
<div
id="purchase-amount"
class="style-scope yt-live-chat-paid-message-renderer"
>
{{ showPriceText }}
</div>
</div>
<span id="timestamp" class="style-scope yt-live-chat-paid-message-renderer">{{ timeText }}</span>
<span
id="timestamp"
class="style-scope yt-live-chat-paid-message-renderer"
>{{ timeText }}</span>
</div>
</div>
<div id="content" class="style-scope yt-live-chat-paid-message-renderer">
<div id="message" dir="auto" class="style-scope yt-live-chat-paid-message-renderer">{{ content }}</div>
<div
id="content"
class="style-scope yt-live-chat-paid-message-renderer"
>
<div
id="message"
dir="auto"
class="style-scope yt-live-chat-paid-message-renderer"
>
{{ content }}
</div>
</div>
</div>
</yt-live-chat-paid-message-renderer>
</template>
<script>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import * as constants from './constants'
import * as utils from './utils'
export default {
name: 'PaidMessage',
components: {
ImgShadow
},
props: {
avatarUrl: String,
authorName: String,
price: Number, // 价格,人民币
priceText: String,
time: Date,
content: String
},
computed: {
priceConfig() {
return constants.getPriceConfig(this.price)
},
color() {
return this.priceConfig.colors
},
showPriceText() {
return this.priceText || `CN¥${utils.formatCurrency(this.price)}`
},
timeText() {
return utils.getTimeTextHourMin(this.time)
}
}
}
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,34 +1,68 @@
<template>
<yt-live-chat-text-message-renderer :author-type="authorTypeText" :blc-guard-level="privilegeType">
<img-shadow id="author-photo" height="24" width="24" class="style-scope yt-live-chat-text-message-renderer"
:imgUrl="avatarUrl"
></img-shadow>
<div id="content" class="style-scope yt-live-chat-text-message-renderer">
<span id="timestamp" class="style-scope yt-live-chat-text-message-renderer">{{ timeText }}</span>
<author-chip class="style-scope yt-live-chat-text-message-renderer"
:isInMemberMessage="false" :authorName="authorName" :authorType="authorType" :privilegeType="privilegeType"
></author-chip>
<span id="message" class="style-scope yt-live-chat-text-message-renderer">
<yt-live-chat-text-message-renderer
:author-type="authorTypeText"
:blc-guard-level="privilegeType"
>
<img-shadow
id="author-photo"
height="24"
width="24"
class="style-scope yt-live-chat-text-message-renderer"
:img-url="avatarUrl"
/>
<div
id="content"
class="style-scope yt-live-chat-text-message-renderer"
>
<span
id="timestamp"
class="style-scope yt-live-chat-text-message-renderer"
>{{ timeText }}</span>
<author-chip
class="style-scope yt-live-chat-text-message-renderer"
:is-in-member-message="false"
:author-name="authorName"
:author-type="authorType"
:privilege-type="privilegeType"
/>
<span
id="message"
class="style-scope yt-live-chat-text-message-renderer"
>
<template v-for="(content, index) in richContent">
<span :key="index" v-if="content.type === CONTENT_TYPE_TEXT">{{ content.text }}</span>
<span
v-if="content.type === CONTENT_TYPE_TEXT"
:key="index"
>{{ content.text }}</span>
<!-- 如果CSS设置的尺寸比属性设置的尺寸还大在图片加载完后布局会变化可能导致滚动卡住没什么好的解决方法 -->
<img :key="'_' + index" v-else-if="content.type === CONTENT_TYPE_IMAGE"
<img
v-else-if="content.type === CONTENT_TYPE_IMAGE"
:id="`emoji-${content.text}`"
:key="'_' + index"
class="emoji yt-formatted-string style-scope yt-live-chat-text-message-renderer"
:src="content.url" :alt="content.text" :shared-tooltip-text="content.text" :id="`emoji-${content.text}`"
:width="content.width" :height="content.height"
:src="content.url"
:alt="content.text"
:shared-tooltip-text="content.text"
:width="content.width"
:height="content.height"
:class="{ 'blc-large-emoji': content.height >= 100 }"
referrerpolicy="no-referrer"
>
</template>
<NBadge :value="repeated" :max="99" v-if="repeated > 1" class="style-scope yt-live-chat-text-message-renderer"
<NBadge
v-if="repeated > 1"
:value="repeated"
:max="99"
class="style-scope yt-live-chat-text-message-renderer"
:style="{ '--repeated-mark-color': repeatedMarkColor }"
></NBadge>
/>
</span>
</div>
</yt-live-chat-text-message-renderer>
</template>
<script>
<script setup>
import { computed } from 'vue'
import ImgShadow from './ImgShadow.vue'
import AuthorChip from './AuthorChip.vue'
import * as constants from './constants'
@@ -39,52 +73,42 @@ import { NBadge } from 'naive-ui'
const REPEATED_MARK_COLOR_START = [210, 100.0, 62.5]
const REPEATED_MARK_COLOR_END = [360, 87.3, 69.2]
export default {
name: 'TextMessage',
components: {
ImgShadow,
AuthorChip,
NBadge
},
props: {
avatarUrl: String,
time: Date,
authorName: String,
authorType: Number,
richContent: Array,
privilegeType: Number,
repeated: Number
},
data() {
return {
CONTENT_TYPE_TEXT: constants.CONTENT_TYPE_TEXT,
CONTENT_TYPE_IMAGE: constants.CONTENT_TYPE_IMAGE
}
},
computed: {
timeText() {
return utils.getTimeTextHourMin(this.time)
},
authorTypeText() {
return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
},
repeatedMarkColor() {
let color
if (this.repeated <= 2) {
color = REPEATED_MARK_COLOR_START
} else if (this.repeated >= 10) {
color = REPEATED_MARK_COLOR_END
} else {
color = [0, 0, 0]
let t = (this.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]}%)`
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,
richContent: 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>

View File

@@ -1,198 +1,243 @@
<!-- 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 tag="div" :css="false" @enter="onTickerItemEnter" @leave="onTickerItemLeave" id="items"
class="style-scope yt-live-chat-ticker-renderer">
<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"
<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"
:imgUrl="message.raw.avatarUrl"></img-shadow>
<span id="text" dir="ltr"
class="style-scope yt-live-chat-ticker-paid-message-item-renderer">{{ message.text }}</span>
: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 :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
class="style-scope yt-live-chat-ticker-renderer" :avatarUrl="pinnedMessage.avatarUrl"
:authorName="getShowAuthorName(pinnedMessage)" :privilegeType="pinnedMessage.privilegeType"
:title="pinnedMessage.title" :time="pinnedMessage.time"></membership-item>
<paid-message :key="pinnedMessage.id" v-else class="style-scope yt-live-chat-ticker-renderer"
:price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="getShowAuthorName(pinnedMessage)"
:time="pinnedMessage.time" :content="pinnedMessageShowContent"></paid-message>
<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>
<script setup>
// @ts-nocheck
import { ref, computed, onBeforeUnmount, nextTick } from 'vue'
import { formatCurrency } from './utils'
import ImgShadow from './ImgShadow.vue'
import MembershipItem from './MembershipItem.vue'
import PaidMessage from './PaidMessage.vue'
import * as constants from './constants'
export default {
name: 'Ticker',
components: {
ImgShadow,
MembershipItem,
PaidMessage
},
props: {
messages: Array,
showGiftName: {
type: Boolean,
default: false
}
},
data() {
return {
MESSAGE_TYPE_MEMBER: constants.MESSAGE_TYPE_MEMBER,
const props = defineProps({
messages: Array,
showGiftName: {
type: Boolean,
default: false
}
})
curTime: new Date(),
updateTimerId: window.setInterval(this.updateProgress, 1000),
pinnedMessage: null
}
},
computed: {
showMessages() {
let res = []
for (let message of this.messages) {
if (!this.needToShow(message)) {
continue
}
res.push({
raw: message,
bgColor: this.getBgColor(message),
color: this.getColor(message),
text: this.getText(message)
})
}
return res
},
pinnedMessageShowContent() {
if (!this.pinnedMessage) {
return ''
}
if (this.pinnedMessage.type === constants.MESSAGE_TYPE_GIFT) {
return constants.getGiftShowContent(this.pinnedMessage, this.showGiftName)
} else {
return constants.getShowContent(this.pinnedMessage)
}
}
},
beforeDestroy() {
window.clearInterval(this.updateTimerId)
},
methods: {
async onTickerItemEnter(el, done) {
let width = el.clientWidth
if (width === 0) {
// CSS指定了不显示固定栏
done()
return
}
el.style.width = 0
await this.$nextTick()
el.style.width = `${width}px`
window.setTimeout(done, 200)
},
onTickerItemLeave(el, done) {
el.classList.add('sliding-down')
window.setTimeout(() => {
el.classList.add('collapsing')
el.style.width = 0
window.setTimeout(() => {
el.classList.remove('sliding-down')
el.classList.remove('collapsing')
el.style.width = 'auto'
done()
}, 200)
}, 200)
},
const emit = defineEmits(['update:messages'])
getShowAuthorName: constants.getShowAuthorName,
needToShow(message) {
let pinTime = this.getPinTime(message)
return (new Date() - message.addTime) / (60 * 1000) < pinTime
},
getBgColor(message) {
let color1, color2
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
color1 = 'rgba(15,157,88,1)'
color2 = 'rgba(11,128,67,1)'
} else {
let config = constants.getPriceConfig(message.price)
color1 = config.colors.contentBg
color2 = config.colors.headerBg
}
let pinTime = this.getPinTime(message)
let progress = (1 - ((this.curTime - message.addTime) / (60 * 1000) / pinTime)) * 100
if (progress < 0) {
progress = 0
} else if (progress > 100) {
progress = 100
}
return `linear-gradient(90deg, ${color1}, ${color1} ${progress}%, ${color2} ${progress}%, ${color2})`
},
getColor(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return 'rgb(255,255,255)'
}
return constants.getPriceConfig(message.price).colors.header
},
getText(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return this.$t('chat.tickerMembership')
}
return `CN¥${formatCurrency(message.price)}`
},
getPinTime(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return 2
}
return constants.getPriceConfig(message.price).pinTime
},
updateProgress() {
// 更新进度
this.curTime = new Date()
const MESSAGE_TYPE_MEMBER = constants.MESSAGE_TYPE_MEMBER
const curTime = ref(new Date())
const pinnedMessage = ref(null)
// 删除过期的消息
let filteredMessages = []
let messagesChanged = false
for (let message of this.messages) {
let pinTime = this.getPinTime(message)
if ((this.curTime - message.addTime) / (60 * 1000) >= pinTime) {
messagesChanged = true
if (this.pinnedMessage === message) {
this.pinnedMessage = null
}
continue
}
filteredMessages.push(message)
}
if (messagesChanged) {
this.$emit('update:messages', filteredMessages)
}
},
onItemClick(message) {
if (this.pinnedMessage == message) {
this.pinnedMessage = null
} else {
this.pinnedMessage = message
}
// 定时更新进度
const updateTimerId = window.setInterval(updateProgress, 1000)
onBeforeUnmount(() => {
window.clearInterval(updateTimerId)
})
const showMessages = computed(() => {
let res = []
for (let message of props.messages) {
if (!needToShow(message)) {
continue
}
res.push({
raw: message,
bgColor: getBgColor(message),
color: getColor(message),
text: getText(message)
})
}
return res
})
const pinnedMessageShowContent = computed(() => {
if (!pinnedMessage.value) {
return ''
}
if (pinnedMessage.value.type === constants.MESSAGE_TYPE_GIFT) {
return constants.getGiftShowContent(pinnedMessage.value, props.showGiftName)
} else {
return constants.getShowContent(pinnedMessage.value)
}
})
async function onTickerItemEnter(el, done) {
let width = el.clientWidth
if (width === 0) {
// CSS指定了不显示固定栏
done()
return
}
el.style.width = 0
await nextTick()
el.style.width = `${width}px`
window.setTimeout(done, 200)
}
function onTickerItemLeave(el, done) {
el.classList.add('sliding-down')
window.setTimeout(() => {
el.classList.add('collapsing')
el.style.width = 0
window.setTimeout(() => {
el.classList.remove('sliding-down')
el.classList.remove('collapsing')
el.style.width = 'auto'
done()
}, 200)
}, 200)
}
const getShowAuthorName = constants.getShowAuthorName
function needToShow(message) {
let pinTime = getPinTime(message)
return (new Date() - message.addTime) / (60 * 1000) < pinTime
}
function getBgColor(message) {
let color1, color2
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
color1 = 'rgba(15,157,88,1)'
color2 = 'rgba(11,128,67,1)'
} else {
let config = constants.getPriceConfig(message.price)
color1 = config.colors.contentBg
color2 = config.colors.headerBg
}
let pinTime = getPinTime(message)
let progress = (1 - ((curTime.value - message.addTime) / (60 * 1000) / pinTime)) * 100
if (progress < 0) {
progress = 0
} else if (progress > 100) {
progress = 100
}
return `linear-gradient(90deg, ${color1}, ${color1} ${progress}%, ${color2} ${progress}%, ${color2})`
}
function getColor(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return 'rgb(255,255,255)'
}
return constants.getPriceConfig(message.price).colors.header
}
function getText(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return '舰长'
}
return `CN¥${formatCurrency(message.price)}`
}
function getPinTime(message) {
if (message.type === constants.MESSAGE_TYPE_MEMBER) {
return 2
}
return constants.getPriceConfig(message.price).pinTime
}
function updateProgress() {
// 更新进度
curTime.value = new Date()
// 删除过期的消息
let filteredMessages = []
let messagesChanged = false
for (let message of props.messages) {
let pinTime = getPinTime(message)
if ((curTime.value - message.addTime) / (60 * 1000) >= pinTime) {
messagesChanged = true
if (pinnedMessage.value === message) {
pinnedMessage.value = null
}
continue
}
filteredMessages.push(message)
}
if (messagesChanged) {
emit('update:messages', filteredMessages)
}
}
function onItemClick(message) {
if (pinnedMessage.value == message) {
pinnedMessage.value = null
} else {
pinnedMessage.value = message
}
}
</script>

View File

@@ -1,4 +1,3 @@
export const AUTHOR_TYPE_NORMAL = 0
export const AUTHOR_TYPE_MEMBER = 1
export const AUTHOR_TYPE_ADMIN = 2
@@ -181,6 +180,17 @@ export function getShowRichContent(message) {
return richContent
}
export function getShowContentParts(message) {
let contentParts = [...message.contentParts || []]
if (message.translation) {
contentParts.push({
type: CONTENT_TYPE_TEXT,
text: `${message.translation}`
})
}
return contentParts
}
export function getGiftShowContent(message, showGiftName) {
if (!showGiftName) {
return ''

View File

@@ -1,3 +1,6 @@
import { format } from 'date-fns'
export function mergeConfig(config, defaultConfig) {
let res = {}
for (let i in defaultConfig) {
@@ -36,9 +39,7 @@ export function formatCurrency(price) {
}
export function getTimeTextHourMin(date) {
let hour = date.getHours()
let min = `00${date.getMinutes()}`.slice(-2)
return `${hour}:${min}`
return format(date, 'H:mm')
}
export function getUuid4Hex() {