Compare commits

..

2 Commits

Author SHA1 Message Date
2fc8f7fcf8 feat: 增强弹幕处理逻辑,支持批量更新和优化过期弹幕移除
- 新增待处理弹幕队列和批量更新功能,提升弹幕添加效率。
- 优化弹幕移除逻辑,使用 filter 方法处理过期弹幕。
- 添加样式优化,提升弹幕展示效果。
2025-04-21 02:01:39 +08:00
89f9cad9a7 feat: 更新问答信息和问题管理组件
- 在 QAInfo 接口中为答案添加了创建时间字段。
- 在 QuestionItem 组件中增加了得分颜色计算函数,优化了得分显示逻辑。
- 更新了问题管理视图,增强了问题的筛选和显示功能,支持更灵活的用户交互。
- 改进了分享卡片的样式和功能,提升了用户体验。
- 增强了 OBS 组件的预览功能,提供了更直观的展示效果。
2025-04-21 01:57:10 +08:00
7 changed files with 1208 additions and 790 deletions

View File

@@ -358,7 +358,7 @@ export interface QAInfo {
sender: UserBasicInfo sender: UserBasicInfo
target: UserBasicInfo target: UserBasicInfo
question: { message: string; image?: string } question: { message: string; image?: string }
answer?: { message: string; image?: string } answer?: { message: string; image?: string, createdAt: number }
isReaded?: boolean isReaded?: boolean
isSenderRegisted: boolean isSenderRegisted: boolean
isPublic: boolean isPublic: boolean

View File

@@ -3,7 +3,7 @@
import { NSpin } from 'naive-ui'; import { NSpin } from 'naive-ui';
import { DANMAKU_WINDOW_BROADCAST_CHANNEL, DanmakuWindowBCData, DanmakuWindowSettings } from './store/useDanmakuWindow'; import { DANMAKU_WINDOW_BROADCAST_CHANNEL, DanmakuWindowBCData, DanmakuWindowSettings } from './store/useDanmakuWindow';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
import ClientDanmakuItem from './ClientDanmakuItem.vue'; import ClientDanmakuItem from './ClientDanmakuItem.vue';
import { TransitionGroup } from 'vue'; // 添加TransitionGroup导入 import { TransitionGroup } from 'vue'; // 添加TransitionGroup导入
@@ -16,6 +16,8 @@
let bc: BroadcastChannel | undefined = undefined; let bc: BroadcastChannel | undefined = undefined;
const setting = ref<DanmakuWindowSettings>(); const setting = ref<DanmakuWindowSettings>();
const danmakuList = ref<TempDanmakuType[]>([]); const danmakuList = ref<TempDanmakuType[]>([]);
const pendingDanmakuQueue = ref<TempDanmakuType[]>([]); // 新增:待处理弹幕队列
const isUpdateScheduled = ref(false); // 新增:是否已安排更新
const maxItems = computed(() => setting.value?.maxDanmakuCount || 50); const maxItems = computed(() => setting.value?.maxDanmakuCount || 50);
const hasItems = computed(() => danmakuList.value.length > 0); const hasItems = computed(() => danmakuList.value.length > 0);
const isInBatchUpdate = ref(false); // 添加批量更新状态标志 const isInBatchUpdate = ref(false); // 添加批量更新状态标志
@@ -52,6 +54,42 @@
} }
} }
// 新增:处理批量更新
function processBatchUpdate() {
if (pendingDanmakuQueue.value.length === 0) {
isUpdateScheduled.value = false;
return;
}
isInBatchUpdate.value = true; // 开始批量更新
const itemsToAdd = pendingDanmakuQueue.value.slice(); // 复制队列
pendingDanmakuQueue.value = []; // 清空队列
// 将新弹幕添加到列表开头
danmakuList.value.unshift(...itemsToAdd);
// 优化超出长度的弹幕处理
if (danmakuList.value.length > maxItems.value) {
danmakuList.value.splice(maxItems.value, danmakuList.value.length - maxItems.value);
}
isUpdateScheduled.value = false;
// 在下一帧 DOM 更新后结束批量更新状态
nextTick(() => {
isInBatchUpdate.value = false;
});
}
// 新增:安排批量更新
function scheduleBatchUpdate() {
if (!isUpdateScheduled.value) {
isUpdateScheduled.value = true;
requestAnimationFrame(processBatchUpdate);
}
}
function addDanmaku(data: EventModel) { function addDanmaku(data: EventModel) {
if (!setting.value) return; if (!setting.value) return;
@@ -77,37 +115,40 @@
disappearAt = Date.now() + setting.value.autoDisappearTime * 1000; disappearAt = Date.now() + setting.value.autoDisappearTime * 1000;
} }
// 为传入的弹幕对象添加一个随机ID和isNew标记 // 为传入的弹幕对象添加一个随机ID和时间戳
const dataWithId = { const dataWithId: TempDanmakuType = {
...data, ...data,
randomId: nanoid(), // 生成一个随机ID randomId: nanoid(),
disappearAt, // 添加消失时间 disappearAt,
timestamp: Date.now(), // 添加时间戳记录插入时间 timestamp: Date.now(),
}; };
danmakuList.value.unshift(dataWithId); // 将弹幕添加到待处理队列,并安排批量更新
pendingDanmakuQueue.value.push(dataWithId);
scheduleBatchUpdate();
// 优化超出长度的弹幕处理 - 改为标记并动画方式移除 //console.log('[DanmakuWindow] 添加弹幕到队列:', dataWithId);
if (danmakuList.value.length > maxItems.value) {
danmakuList.value.splice(maxItems.value, danmakuList.value.length - maxItems.value);
}
//console.log('[DanmakuWindow] 添加弹幕:', dataWithId);
} }
// 检查和移除过期弹幕 // 检查和移除过期弹幕 - 优化为 filter
function checkAndRemoveExpiredDanmaku() { function checkAndRemoveExpiredDanmaku() {
if (!setting.value || setting.value.autoDisappearTime <= 0) return; if (!setting.value || setting.value.autoDisappearTime <= 0 || danmakuList.value.length === 0) return;
const now = Date.now(); const now = Date.now();
const animationDuration = setting.value.animationDuration || 300; const originalLength = danmakuList.value.length;
// 先标记将要消失的弹幕 danmakuList.value = danmakuList.value.filter(item => {
danmakuList.value.forEach(item => { // 如果没有 disappearAt 或 disappearAt 在未来,则保留
if (item.disappearAt && item.disappearAt <= now && !pendingRemovalItems.value.includes(item.randomId)) { return !item.disappearAt || item.disappearAt > now;
danmakuList.value.splice(danmakuList.value.indexOf(item), 1);
}
}); });
// 如果有弹幕被移除,可以考虑触发一次批量状态(可选,取决于是否需要移除动画也加速)
// if (danmakuList.value.length < originalLength) {
// isInBatchUpdate.value = true;
// nextTick(() => {
// isInBatchUpdate.value = false;
// });
// }
} }
// 为弹幕项生成自定义属性值 // 为弹幕项生成自定义属性值
@@ -341,4 +382,9 @@
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
/* 特殊强调效果 */ /* 特殊强调效果 */
} }
.danmaku-item {
/* 添加 will-change 提示浏览器进行优化 */
will-change: transform, opacity;
}
</style> </style>

View File

@@ -11,6 +11,21 @@ const useQA = useQuestionBox()
const isViolation = props.item.reviewResult?.isApproved == false const isViolation = props.item.reviewResult?.isApproved == false
const showContent = ref(!isViolation) const showContent = ref(!isViolation)
// 计算得分颜色的函数
function getScoreColor(score: number | undefined): string {
if (score === undefined) {
return 'grey'; // 如果没有分数,返回灰色
}
// 将分数限制在 0 到 100 之间
const clampedScore = Math.max(0, Math.min(100, score));
// 插值计算色相: 0 (红色) for score 0, 120 (绿色) for score 100
const hue = 120 * (clampedScore / 100); // 反转插值逻辑
// 固定饱和度和亮度 (可根据需要调整)
const saturation = 50;
const lightness = 45; // 稍暗以提高与白色文本的对比度
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
</script> </script>
<template> <template>
@@ -101,18 +116,18 @@ const showContent = ref(!isViolation)
</NTag> </NTag>
</NFlex> </NFlex>
</template> </template>
<template v-if="item.reviewResult && item.reviewResult.saftyScore"> <template v-if="item.reviewResult && item.reviewResult.saftyScore !== undefined">
<NDivider vertical /> <NDivider vertical />
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NTag <NTag
size="small" size="small"
:color="{ color: '#af2525', textColor: 'white', borderColor: 'white' }" :style="{ backgroundColor: getScoreColor(item.reviewResult.saftyScore), color: 'white', borderColor: 'transparent' }"
> >
得分: {{ item.reviewResult.saftyScore }} 得分: {{ item.reviewResult.saftyScore }}
</NTag> </NTag>
</template> </template>
审查得分, 满分100, 越低代表消息越8行 审查得分, 满分100, 越低代表消息越安全, 越高越危险
</NTooltip> </NTooltip>
</template> </template>
</NFlex> </NFlex>
@@ -139,18 +154,15 @@ const showContent = ref(!isViolation)
<br> <br>
</template> </template>
<NText :style="{ filter: showContent ? '' : 'blur(3.7px)', cursor: showContent ? '' : 'pointer', whiteSpace: 'pre-wrap' }"> <NText
<NButton :style="{
v-if="isViolation" filter: isViolation && !showContent ? 'blur(3.7px)' : '',
size="small" cursor: isViolation && !showContent ? 'pointer' : '',
text whiteSpace: 'pre-wrap'
@click="showContent = !showContent" }"
> @click="isViolation ? (showContent = !showContent) : null"
{{ item.question?.message }} >
</NButton> {{ item.question?.message }}
<template v-else>
{{ item.question?.message }}
</template>
</NText> </NText>
<template v-if="item.answer"> <template v-if="item.answer">

View File

@@ -23,7 +23,11 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
const recieveQuestions = ref<QAInfo[]>([]) const recieveQuestions = ref<QAInfo[]>([])
const sendQuestions = ref<QAInfo[]>([]) const sendQuestions = ref<QAInfo[]>([])
const trashQuestions = ref<QAInfo[]>([]) const trashQuestions = computed(() => {
return recieveQuestions.value.filter(
(q) => q.reviewResult && q.reviewResult.isApproved == false
)
})
const tags = ref<QATagInfo[]>([]) const tags = ref<QATagInfo[]>([])
const reviewing = ref(0) const reviewing = ref(0)
@@ -37,6 +41,7 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
return false return false
}*/ }*/
return ( return (
(!q.reviewResult || q.reviewResult.isApproved == true) &&
(q.isFavorite || !onlyFavorite.value) && (q.isFavorite || !onlyFavorite.value) &&
(q.isPublic || !onlyPublic.value) && (q.isPublic || !onlyPublic.value) &&
(!q.isReaded || !onlyUnread.value) && (!q.isReaded || !onlyUnread.value) &&
@@ -66,16 +71,9 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
if (data.data.questions.length > 0) { if (data.data.questions.length > 0) {
recieveQuestions.value = new List(data.data.questions) recieveQuestions.value = new List(data.data.questions)
.OrderBy((d) => d.isReaded) .OrderBy((d) => d.isReaded)
//.ThenByDescending(d => d.isFavorite)
.Where(
(d) => !d.reviewResult || d.reviewResult.isApproved == true
) //只显示审核通过的
.ThenByDescending((d) => d.sendAt) .ThenByDescending((d) => d.sendAt)
.ToArray() .ToArray()
reviewing.value = data.data.reviewCount reviewing.value = data.data.reviewCount
trashQuestions.value = data.data.questions.filter(
(d) => d.reviewResult && d.reviewResult.isApproved == false
)
const displayId = const displayId =
accountInfo.value?.settings.questionDisplay.currentQuestion accountInfo.value?.settings.questionDisplay.currentQuestion
@@ -378,6 +376,20 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
message.error('拉黑失败: ' + err) message.error('拉黑失败: ' + err)
}) })
} }
async function markAsNormal(question: QAInfo) {
await QueryGetAPI(QUESTION_API_URL + 'mark-as-normal', {
id: question.id
})
.then((data) => {
if (data.code == 200) {
message.success('已标记为正常')
question.reviewResult!.isApproved = true
}
})
.catch((err) => {
message.error('标记失败: ' + err)
})
}
async function setCurrentQuestion(item: QAInfo | undefined) { async function setCurrentQuestion(item: QAInfo | undefined) {
const isCurrent = displayQuestion.value?.id == item?.id const isCurrent = displayQuestion.value?.id == item?.id
if (!isCurrent) { if (!isCurrent) {
@@ -433,6 +445,7 @@ export const useQuestionBox = defineStore('QuestionBox', () => {
favorite, favorite,
setPublic, setPublic,
blacklist, blacklist,
markAsNormal,
setCurrentQuestion, setCurrentQuestion,
getViolationString getViolationString
} }

View File

@@ -224,7 +224,11 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
skipNegotiation: true, skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets transport: signalR.HttpTransportType.WebSockets
}) })
.withAutomaticReconnect([0, 2000, 10000, 30000]) // 自动重连策略 .withAutomaticReconnect({
nextRetryDelayInMilliseconds: retryContext => {
return retryContext.elapsedMilliseconds < 60 * 1000 ? 10 * 1000 : 30 * 1000;
}
}) // 自动重连策略
.withHubProtocol(new msgpack.MessagePackHubProtocol()) // 使用 MessagePack 协议 .withHubProtocol(new msgpack.MessagePackHubProtocol()) // 使用 MessagePack 协议
.build(); .build();
@@ -249,7 +253,7 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`); console.error(prefix.value + `与服务器连接关闭: ${error?.message || '未知原因'}. 自动重连将处理.`);
state.value = 'connecting'; // 标记为连接中,等待自动重连 state.value = 'connecting'; // 标记为连接中,等待自动重连
signalRConnectionId.value = undefined; signalRConnectionId.value = undefined;
// withAutomaticReconnect 会处理重连,这里不需要手动调用 reconnect await connection.start();
} else if (disconnectedByServer) { } else if (disconnectedByServer) {
console.log(prefix.value + `连接已被服务器关闭.`); console.log(prefix.value + `连接已被服务器关闭.`);
//Stop(); // 服务器要求断开,则彻底停止 //Stop(); // 服务器要求断开,则彻底停止
@@ -369,12 +373,6 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
* 定期将队列中的事件发送到服务器 * 定期将队列中的事件发送到服务器
*/ */
async function sendEvents() { async function sendEvents() {
if (updateCount % 60 == 0) {
// 每60秒更新一次连接信息
if (signalRClient.value) {
await sendSelfInfo(signalRClient.value);
}
}
updateCount++; updateCount++;
// 确保 SignalR 已连接 // 确保 SignalR 已连接
if (!signalRClient.value || signalRClient.value.state !== signalR.HubConnectionState.Connected) { if (!signalRClient.value || signalRClient.value.state !== signalR.HubConnectionState.Connected) {
@@ -384,6 +382,12 @@ export const useWebFetcher = defineStore('WebFetcher', () => {
if (events.length === 0) { if (events.length === 0) {
return; return;
} }
if (updateCount % 60 == 0) {
// 每60秒更新一次连接信息
if (signalRClient.value) {
await sendSelfInfo(signalRClient.value);
}
}
// 批量处理事件每次最多发送20条 // 批量处理事件每次最多发送20条
const batchSize = 30; const batchSize = 30;

View File

@@ -645,7 +645,7 @@ const fetchAnalyzeData = async () => {
message.error(`获取数据失败: ${data.message}`); message.error(`获取数据失败: ${data.message}`);
} }
} catch (error) { } catch (error) {
message.error('请求失败,请检查网络连接'); message.error('获取数据出错:' + (error as Error).message);
console.error('获取数据出错:', error); console.error('获取数据出错:', error);
} finally { } finally {
loading.value = false; loading.value = false;

File diff suppressed because it is too large Load Diff