mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-10 20:36:55 +08:00
feat: 更新项目配置和依赖,增强功能和用户体验
- 完成弹幕机功能 - 在 .editorconfig 中新增对 vine.ts 文件的支持。 - 更新 package.json 中多个依赖的版本,提升稳定性和性能。 - 在 vite.config.mts 中引入 @guolao/vue-monaco-editor 插件,增强代码编辑功能。 - 在 App.vue 中调整内容填充的样式,优化界面布局。 - 新增获取配置文件哈希的 API 方法,提升配置管理能力。 - 在多个组件中优化了样式和逻辑,提升用户交互体验。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user