refactor: 优化多个视图组件并添加功能

本次提交对多个视图组件进行了重构和功能增强:

    PointGoodsView.vue:
    - 清理了未使用的导入(`useAccount`)和变量(`accountInfo`, `biliInfo` prop)。
    - 通过重组计算属性和方法提高了代码可读性。
    - 增强了商品列表的筛选和排序逻辑。
    - 为购买商品功能添加了错误处理和加载状态。

    PointUserHistoryView.vue:
    - 为获取积分历史记录实现了加载状态。
    - 改进了 PointHistoryCard 组件的渲染。

    QuestionBoxView.vue:
    - 优化了可读性和性能(整合状态变量,改进命名)。
    - 增强了文件上传处理和验证逻辑。
    - 改进了标签选择逻辑和数据获取方法。
    - 添加了代码注释以提高可理解性。

    UserIndexView.vue:
    - 简化了确定要显示的模板组件的逻辑。
    - 确保无论用户信息是否存在,都一致返回默认模板。
This commit is contained in:
2025-04-17 02:15:22 +08:00
parent 1ea4404307
commit 2e5e0afd30
23 changed files with 4747 additions and 3080 deletions

View File

@@ -6,7 +6,6 @@ import MessageRender from './blivechat/MessageRender.vue';
import * as constants from './blivechat/constants';
// @ts-ignore
import * as pronunciation from './blivechat/utils/pronunciation';
// @ts-ignore
import { DownloadConfig, useAccount } from '@/api/account';
import { QueryGetAPI } from '@/api/query';
import { VTSURU_API_URL } from '@/data/constants';
@@ -54,6 +53,7 @@ const pronunciationConverter = new pronunciation.PronunciationConverter()
const accountInfo = useAccount()
const route = useRoute()
// 默认配置
const defaultConfig: DanmujiConfig = {
minGiftPrice: 0.1,
showDanmaku: true,
@@ -72,15 +72,17 @@ const defaultConfig: DanmujiConfig = {
importPresetCss: false,
emoticons: []
} as DanmujiConfig
}
let textEmoticons: { keyword: string, url: string }[] = []
const config = ref<DanmujiConfig>(JSON.parse(JSON.stringify(defaultConfig)))
const rtc = await useWebRTC().Init('slave')
// 表情词典树计算
const emoticonsTrie = computed(() => {
let res = new trie.Trie()
for (let emoticons of [config.value.emoticons, textEmoticons]) {
for (let emoticon of emoticons) {
const res = new trie.Trie()
for (const emoticons of [config.value.emoticons, textEmoticons]) {
for (const emoticon of emoticons) {
if (emoticon.keyword !== '' && emoticon.url !== '') {
res.set(emoticon.keyword, emoticon)
}
@@ -88,10 +90,12 @@ const emoticonsTrie = computed(() => {
}
return res
})
// 屏蔽关键词词典树计算
const blockKeywordsTrie = computed(() => {
let blockKeywords = config.value.blockKeywords.split('\n')
let res = new trie.Trie()
for (let keyword of blockKeywords) {
const blockKeywords = config.value.blockKeywords.split('\n')
const res = new trie.Trie()
for (const keyword of blockKeywords) {
if (keyword !== '') {
res.set(keyword, true)
}
@@ -99,22 +103,28 @@ const blockKeywordsTrie = computed(() => {
return res
})
/**
* 设置自定义CSS
*/
function setCss(css: string) {
messageRender.value?.setCss(css)
}
/** @param {chatModels.AddTextMsg} data */
/**
* 处理弹幕消息
*/
async function onAddText(data: DanmakuInfo, command: unknown) {
if (!config.value.showDanmaku || !filterTextMessage(data)) {
return
}
let richContent = await getRichContent(data)
const richContent = await getRichContent(data)
// 合并要放在异步调用后面,因为异步调用后可能有新的消息,会漏合并
if (mergeSimilarText(data.msg)) {
return
}
let message = {
const message = {
id: data.msg_id,
type: constants.MESSAGE_TYPE_TEXT,
avatarUrl: data.uface,
@@ -129,19 +139,27 @@ async function onAddText(data: DanmakuInfo, command: unknown) {
}
messageRender.value.addMessage(message)
}
/** @param {chatModels.AddGiftMsg} data */
function onAddGift(data: GiftInfo, command: any) {
/**
* 处理礼物消息
*/
function onAddGift(data: GiftInfo, command: unknown) {
if (!config.value.showGift) {
return
}
let price = (data.price * data.gift_num) / 1000
const price = (data.price * data.gift_num) / 1000
// 价格过滤
if (price < (config.value.minGiftPrice ?? 0)) {
return
}
// 尝试合并相似礼物
if (mergeSimilarGift(data.uname, price, !data.paid ? price : 0, data.gift_name, data.gift_num)) {
return
}
if (price < (config.value.minGiftPrice ?? 0)) { // 丢人
return
}
let message = {
const message = {
id: data.msg_id,
type: constants.MESSAGE_TYPE_GIFT,
avatarUrl: data.uface,
@@ -149,18 +167,21 @@ function onAddGift(data: GiftInfo, command: any) {
authorName: data.uname,
authorNamePronunciation: getPronunciation(data.uname),
price: price,
// freePrice: data.totalFreeCoin, // 暂时没用到
giftName: data.gift_name,
num: data.gift_num
}
messageRender.value.addMessage(message)
}
/** @param {chatModels.AddMemberMsg} data */
function onAddMember(data: GuardInfo, command: any) {
/**
* 处理舰长上舰消息
*/
function onAddMember(data: GuardInfo, command: unknown) {
if (!config.value.showGift || !filterNewMemberMessage(data)) {
return
}
let message = {
const message = {
id: data.msg_id,
type: constants.MESSAGE_TYPE_MEMBER,
avatarUrl: data.user_info.uface,
@@ -172,15 +193,20 @@ function onAddMember(data: GuardInfo, command: any) {
}
messageRender.value.addMessage(message)
}
/** @param {chatModels.AddSuperChatMsg} data */
/**
* 处理醒目留言消息
*/
function onAddSuperChat(data: SCInfo) {
if (!config.value.showGift || !filterSuperChatMessage(data)) {
return
}
if (data.rmb < (config.value.minGiftPrice ?? 0)) { // 丢人
if (data.rmb < (config.value.minGiftPrice ?? 0)) {
return
}
let message = {
const message = {
id: data.msg_id,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.uface,
@@ -193,29 +219,41 @@ function onAddSuperChat(data: SCInfo) {
}
messageRender.value.addMessage(message)
}
/** @param {chatModels.DelSuperChatMsg} data */
function onDelSuperChat(data: any) {
/**
* 处理SC撤回
*/
function onDelSuperChat(data: { id: string }) {
messageRender.value.deleteMessage(data.id)
}
function getAuthorType(open_id: string, guard_level: number) {
let authorType
/**
* 获取用户类型0-普通用户1-舰长3-主播
*/
function getAuthorType(open_id: string, guard_level: number): number {
if (open_id === client.authInfo?.anchor_info.open_id) {
authorType = 3
return 3 // 主播
} else if (guard_level !== 0) {
authorType = 1
return 1 // 舰长
} else {
authorType = 0
return 0 // 普通用户
}
}
type RichContentType = {
type: string,
type: number,
text: string,
url?: string,
width?: number,
height?: number
}
async function getRichContent(data: DanmakuInfo) {
let richContent: RichContentType[] = []
/**
* 获取富文本内容(处理表情等)
*/
async function getRichContent(data: DanmakuInfo): Promise<RichContentType[]> {
const richContent: RichContentType[] = []
// 官方的非文本表情
if (data.emoji_img_url) {
richContent.push({
@@ -225,7 +263,6 @@ async function getRichContent(data: DanmakuInfo) {
width: 256,
height: 256
})
//await fillImageContentSizes(richContent)
return richContent
}
@@ -242,8 +279,8 @@ async function getRichContent(data: DanmakuInfo) {
let startPos = 0
let pos = 0
while (pos < data.msg.length) {
let remainContent = data.msg.substring(pos)
let matchEmoticon = emoticonsTrie.value.lazyMatch(remainContent)
const remainContent = data.msg.substring(pos)
const matchEmoticon = emoticonsTrie.value.lazyMatch(remainContent)
if (matchEmoticon === null) {
pos++
continue
@@ -268,6 +305,7 @@ async function getRichContent(data: DanmakuInfo) {
pos += matchEmoticon.keyword.length
startPos = pos
}
// 加入尾部的文本
if (pos !== startPos) {
richContent.push({
@@ -279,131 +317,175 @@ async function getRichContent(data: DanmakuInfo) {
await fillImageContentSizes(richContent)
return richContent
}
/**
* 填充图片内容的尺寸信息
*/
async function fillImageContentSizes(richContent: RichContentType[]) {
let urlSizeMap = new Map()
for (let content of richContent) {
if (content.type === constants.CONTENT_TYPE_IMAGE) {
const urlSizeMap = new Map()
// 收集所有需要获取尺寸的图片URL
for (const content of richContent) {
if (content.type === constants.CONTENT_TYPE_IMAGE && content.url) {
urlSizeMap.set(content.url, { width: 0, height: 0 })
}
}
if (urlSizeMap.size === 0) {
return
}
let promises = []
for (let url of urlSizeMap.keys()) {
let urlInClosure = url
promises.push(new Promise(
resolve => {
let img = document.createElement('img')
img.onload = () => {
let size = urlSizeMap.get(urlInClosure)
size.width = img.naturalWidth
size.height = img.naturalHeight
// @ts-expect-error 忽略这里测错误
resolve()
}
// 获取失败了默认为0
img.onerror = resolve
// 超时保底
window.setTimeout(resolve, 5000)
img.src = urlInClosure
// 并行加载所有图片获取尺寸
const promises = []
for (const url of urlSizeMap.keys()) {
promises.push(new Promise<void>(resolve => {
const img = document.createElement('img')
img.onload = () => {
const size = urlSizeMap.get(url)
size.width = img.naturalWidth
size.height = img.naturalHeight
resolve()
}
))
// 获取失败了默认为0
img.onerror = () => resolve()
// 超时保底
window.setTimeout(() => resolve(), 5000)
img.src = url
}))
}
await Promise.all(promises)
for (let content of richContent) {
if (content.type === constants.CONTENT_TYPE_IMAGE) {
let size = urlSizeMap.get(content.url)
// 应用获取的尺寸到富文本内容
for (const content of richContent) {
if (content.type === constants.CONTENT_TYPE_IMAGE && content.url) {
const size = urlSizeMap.get(content.url)
content.width = size.width
content.height = size.height
}
}
}
function getPronunciation(text: string) {
if (pronunciationConverter === null) {
/**
* 获取名称发音
*/
function getPronunciation(text: string): string {
if (!pronunciationConverter) {
return ''
}
return pronunciationConverter.getPronunciation(text)
}
function filterSuperChatMessage(data: SCInfo) {
/**
* 过滤SC消息
*/
function filterSuperChatMessage(data: SCInfo): boolean {
return filterByContent(data.message) && filterByAuthorName(data.uname)
}
function filterNewMemberMessage(data: GuardInfo) {
/**
* 过滤新舰长消息
*/
function filterNewMemberMessage(data: GuardInfo): boolean {
return filterByAuthorName(data.user_info.uname)
}
function filterByContent(content: string) {
/**
* 根据内容过滤消息
*/
function filterByContent(content: string): boolean {
for (let i = 0; i < content.length; i++) {
let remainContent = content.substring(i)
const remainContent = content.substring(i)
if (blockKeywordsTrie.value.lazyMatch(remainContent) !== null) {
return false
}
}
return true
}
function filterByAuthorName(id: string) {
return Object.keys(accountInfo.value.biliBlackList).indexOf(id) === -1
/**
* 根据用户名过滤消息
*/
function filterByAuthorName(id: string): boolean {
return !(id in accountInfo.value.biliBlackList)
}
function filterTextMessage(data: DanmakuInfo) {
/**
* 过滤弹幕消息
*/
function filterTextMessage(data: DanmakuInfo): boolean {
// 舰长等级过滤
if (config.value.blockLevel > 0 && data.guard_level < config.value.blockLevel) {
return false
} else if (config.value.blockMedalLevel > 0 && data.fans_medal_level < config.value.blockMedalLevel) {
}
// 粉丝牌等级过滤
else if (config.value.blockMedalLevel > 0 && data.fans_medal_level < config.value.blockMedalLevel) {
return false
}
return filterByContent(data.msg) && filterByAuthorName(data.uname)
}
function mergeSimilarText(content: string) {
/**
* 合并相似文本
*/
function mergeSimilarText(content: string): boolean {
if (!config.value.mergeSimilarDanmaku) {
return false
}
return messageRender.value.mergeSimilarText(content)
}
function mergeSimilarGift(authorName: string, price: number, freePrice: number, giftName: string, num: number) {
/**
* 合并相似礼物
*/
function mergeSimilarGift(authorName: string, price: number, freePrice: number, giftName: string, num: number): boolean {
if (!config.value.mergeGift) {
return false
}
return messageRender.value.mergeSimilarGift(authorName, price, freePrice, giftName, num)
}
/**
* 接收配置更新
*/
function onReceiveConfig(data: DanmujiConfig) {
config.value = data
}
onMounted(async () => {
// 注册事件监听
client.on('danmaku', onAddText)
client.on('gift', onAddGift)
client.on('sc', onAddSuperChat)
//client.onEvent('delsc', onSuperChatDel)
client.on('guard', onAddMember)
rtc?.on('danmuji.config', onReceiveConfig)
// 注册RTC配置接收
if (rtc) {
rtc.on('danmuji.config', onReceiveConfig)
}
QueryGetAPI<{ keyword: string, url: string }[]>(VTSURU_API_URL + 'blivechat/emoticon').then((data) => {
if (data.code === 200) {
textEmoticons = data.data
}
})
return
while (true) {
const result = await DownloadConfig('OBS.Danmuji')
if (result.msg === undefined) {
config.value = result.data as DanmujiConfig
break
}
else if (result.status == 'error') {
await new Promise(resolve => setTimeout(resolve, 1000))
// 加载表情包
try {
const result = await QueryGetAPI<{ keyword: string, url: string }[]>(VTSURU_API_URL + 'blivechat/emoticon')
if (result.code === 200) {
textEmoticons = result.data
}
} catch (error) {
console.error('加载表情包失败:', error)
}
})
onUnmounted(() => {
// 取消事件监听
client.off('danmaku', onAddText)
client.off('gift', onAddGift)
client.off('sc', onAddSuperChat)
//client.offEvent('delsc', onSuperChatDel)
client.off('guard', onAddMember)
rtc?.off('danmuji.config', onReceiveConfig)
// 取消RTC配置接收
if (rtc) {
rtc.off('danmuji.config', onReceiveConfig)
}
})
</script>