mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
Compare commits
2 Commits
aa2d63a33c
...
2fc8f7fcf8
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fc8f7fcf8 | |||
| 89f9cad9a7 |
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 }}
|
{{ item.question?.message }}
|
||||||
</NButton>
|
|
||||||
<template v-else>
|
|
||||||
{{ item.question?.message }}
|
|
||||||
</template>
|
|
||||||
</NText>
|
</NText>
|
||||||
|
|
||||||
<template v-if="item.answer">
|
<template v-if="item.answer">
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user