mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-12-08 13:49:37 +00:00
feat: add urgent notification feature and improve UI components
- Updated index.vue to include an urgent notification button and dialog. - Added UrgentTestDialog.vue component for sending notifications with urgency options. - Enhanced UI elements for better user experience, including improved spacing and layout. - Implemented notification history display with receipt tracking for sent messages. - Added methods for handling notification sending and receipt updates via socket events.
This commit is contained in:
parent
ca4de545b9
commit
6c990bd8e4
@ -13,7 +13,7 @@
|
|||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="urgent-title mb-6">
|
<div class="urgent-title mb-6">
|
||||||
{{ notification?.content?.message || "无内容" }}
|
{{ currentNotification?.content?.message || "无内容" }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 发送者信息(使用 Vuetify Card) -->
|
<!-- 发送者信息(使用 Vuetify Card) -->
|
||||||
@ -45,11 +45,47 @@
|
|||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
<v-icon left size="16"> mdi-clock </v-icon>
|
<v-icon left size="16"> mdi-clock </v-icon>
|
||||||
{{ formatTime(notification?.timestamp) }}
|
{{ formatTime(currentNotification?.timestamp) }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 多通知导航 -->
|
||||||
|
<div v-if="hasMultipleNotifications" class="navigation-controls mt-6">
|
||||||
|
<v-card variant="flat" color="rgba(255,255,255,0.1)">
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<div class="notification-counter mb-3">
|
||||||
|
<v-chip color="white" variant="flat" size="small">
|
||||||
|
{{ notificationCountText }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-buttons">
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentIndex === 0"
|
||||||
|
color="white"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
@click="previousNotification"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-chevron-left </v-icon>
|
||||||
|
上一个
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentIndex === notificationQueue.length - 1"
|
||||||
|
color="white"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
@click="nextNotification"
|
||||||
|
>
|
||||||
|
下一个
|
||||||
|
<v-icon> mdi-chevron-right </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<v-btn color="white" size="large" variant="flat" @click="close">
|
<v-btn color="white" size="large" variant="flat" @click="close">
|
||||||
@ -76,14 +112,32 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
visible: false,
|
visible: false,
|
||||||
notification: null,
|
notificationQueue: [], // 通知队列
|
||||||
|
currentIndex: 0, // 当前显示的通知索引
|
||||||
autoCloseTimer: null,
|
autoCloseTimer: null,
|
||||||
urgentSoundTimer: null,
|
urgentSoundTimer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// 当前显示的通知
|
||||||
|
currentNotification() {
|
||||||
|
return this.notificationQueue[this.currentIndex] || null;
|
||||||
|
},
|
||||||
|
// 队列中是否有通知
|
||||||
|
hasNotifications() {
|
||||||
|
return this.notificationQueue.length > 0;
|
||||||
|
},
|
||||||
|
// 是否有多个通知
|
||||||
|
hasMultipleNotifications() {
|
||||||
|
return this.notificationQueue.length > 1;
|
||||||
|
},
|
||||||
|
// 通知计数文本
|
||||||
|
notificationCountText() {
|
||||||
|
if (!this.hasMultipleNotifications) return "";
|
||||||
|
return `${this.currentIndex + 1} / ${this.notificationQueue.length}`;
|
||||||
|
},
|
||||||
isUrgent() {
|
isUrgent() {
|
||||||
return this.notification?.content?.isUrgent || false;
|
return this.currentNotification?.content?.isUrgent || false;
|
||||||
},
|
},
|
||||||
urgencyColor() {
|
urgencyColor() {
|
||||||
return this.isUrgent ? "red darken-2" : "blue darken-2";
|
return this.isUrgent ? "red darken-2" : "blue darken-2";
|
||||||
@ -101,18 +155,20 @@ export default {
|
|||||||
},
|
},
|
||||||
senderName() {
|
senderName() {
|
||||||
const senderInfo =
|
const senderInfo =
|
||||||
this.notification?.senderInfo || this.notification?.content?.senderInfo;
|
this.currentNotification?.senderInfo ||
|
||||||
|
this.currentNotification?.content?.senderInfo;
|
||||||
if (!senderInfo) return "未知发送者";
|
if (!senderInfo) return "未知发送者";
|
||||||
|
|
||||||
return senderInfo.deviceName || senderInfo.deviceType || "未知设备";
|
return senderInfo.deviceName || senderInfo.deviceType || "未知设备";
|
||||||
},
|
},
|
||||||
deviceType() {
|
deviceType() {
|
||||||
const senderInfo =
|
const senderInfo =
|
||||||
this.notification?.senderInfo || this.notification?.content?.senderInfo;
|
this.currentNotification?.senderInfo ||
|
||||||
|
this.currentNotification?.content?.senderInfo;
|
||||||
return senderInfo?.deviceType || "未知类型";
|
return senderInfo?.deviceType || "未知类型";
|
||||||
},
|
},
|
||||||
targetDevices() {
|
targetDevices() {
|
||||||
return this.notification?.content?.targetDevices || [];
|
return this.currentNotification?.content?.targetDevices || [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@ -125,24 +181,40 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show(notificationData) {
|
show(notificationData) {
|
||||||
this.notification = notificationData;
|
// 检查是否已存在相同的通知(避免重复)
|
||||||
this.visible = true;
|
const existingIndex = this.notificationQueue.findIndex(
|
||||||
|
(n) =>
|
||||||
|
n.content?.notificationId === notificationData.content?.notificationId
|
||||||
|
);
|
||||||
|
|
||||||
// 发送显示回执
|
if (existingIndex !== -1) {
|
||||||
this.sendDisplayedReceipt();
|
console.log("通知已存在,跳过添加");
|
||||||
|
return;
|
||||||
// 清除之前的自动关闭定时器
|
|
||||||
if (this.autoCloseTimer) {
|
|
||||||
clearTimeout(this.autoCloseTimer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放统一的提示音
|
// 添加到队列
|
||||||
|
this.notificationQueue.push(notificationData);
|
||||||
|
|
||||||
|
// 如果当前没有显示通知,显示第一个
|
||||||
|
if (!this.visible) {
|
||||||
|
this.currentIndex = this.notificationQueue.length - 1; // 显示最新的通知
|
||||||
|
this.visible = true;
|
||||||
|
this.sendDisplayedReceipt();
|
||||||
this.playNotificationSound();
|
this.playNotificationSound();
|
||||||
|
|
||||||
// 如果是加急通知,启动定时音效
|
// 如果是加急通知,启动定时音效
|
||||||
if (this.isUrgent) {
|
if (this.isUrgent) {
|
||||||
this.startUrgentSound();
|
this.startUrgentSound();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 如果已经有通知在显示,新通知是紧急的话优先显示
|
||||||
|
if (notificationData.content?.isUrgent && !this.isUrgent) {
|
||||||
|
this.currentIndex = this.notificationQueue.length - 1;
|
||||||
|
this.sendDisplayedReceipt();
|
||||||
|
this.playNotificationSound();
|
||||||
|
this.startUrgentSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
// 只在用户主动关闭时发送已读回执
|
// 只在用户主动关闭时发送已读回执
|
||||||
@ -153,24 +225,58 @@ export default {
|
|||||||
console.warn("发送已读回执失败:", error);
|
console.warn("发送已读回执失败:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示已读消息提示,便于设备端重新查看
|
||||||
|
if (this.currentNotification?.content?.message) {
|
||||||
|
const notificationType = this.isUrgent ? "紧急通知" : "通知";
|
||||||
|
if (this.isUrgent) {
|
||||||
|
this.$message?.error(
|
||||||
|
notificationType,
|
||||||
|
`${this.currentNotification.content.message}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$message?.info(
|
||||||
|
notificationType,
|
||||||
|
`${this.currentNotification.content.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从队列中移除当前通知
|
||||||
|
if (this.notificationQueue.length > 0) {
|
||||||
|
this.notificationQueue.splice(this.currentIndex, 1);
|
||||||
|
|
||||||
|
// 调整当前索引
|
||||||
|
if (this.currentIndex >= this.notificationQueue.length) {
|
||||||
|
this.currentIndex = Math.max(0, this.notificationQueue.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还有通知,显示下一个;否则关闭
|
||||||
|
if (this.notificationQueue.length > 0) {
|
||||||
|
this.sendDisplayedReceipt();
|
||||||
|
// 如果新的当前通知是紧急的,启动音效
|
||||||
|
if (this.isUrgent) {
|
||||||
|
this.startUrgentSound();
|
||||||
|
} else {
|
||||||
|
this.stopUrgentSound();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
this.closeWithoutRead();
|
this.closeWithoutRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 关闭通知但不发送已读回执(用于程序异常或强制关闭)
|
// 关闭通知但不发送已读回执(用于程序异常或强制关闭)
|
||||||
closeWithoutRead() {
|
closeWithoutRead() {
|
||||||
// 立即关闭弹框
|
// 立即关闭弹框
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.notification = null;
|
this.notificationQueue = [];
|
||||||
|
this.currentIndex = 0;
|
||||||
|
|
||||||
if (this.autoCloseTimer) {
|
if (this.autoCloseTimer) {
|
||||||
clearTimeout(this.autoCloseTimer);
|
clearTimeout(this.autoCloseTimer);
|
||||||
this.autoCloseTimer = null;
|
this.autoCloseTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止加急音效定时器
|
this.stopUrgentSound();
|
||||||
if (this.urgentSoundTimer) {
|
|
||||||
clearInterval(this.urgentSoundTimer);
|
|
||||||
this.urgentSoundTimer = null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
formatTime(timestamp) {
|
formatTime(timestamp) {
|
||||||
if (!timestamp) return "";
|
if (!timestamp) return "";
|
||||||
@ -230,12 +336,12 @@ export default {
|
|||||||
// 发送显示回执
|
// 发送显示回执
|
||||||
sendDisplayedReceipt() {
|
sendDisplayedReceipt() {
|
||||||
try {
|
try {
|
||||||
if (this.$refs.eventSender && this.notification?.eventId) {
|
if (this.$refs.eventSender && this.currentNotification?.eventId) {
|
||||||
this.$refs.eventSender.sendDisplayedReceipt(
|
this.$refs.eventSender.sendDisplayedReceipt(
|
||||||
{},
|
{},
|
||||||
this.notification.content.notificationId
|
this.currentNotification.content.notificationId
|
||||||
);
|
);
|
||||||
console.log("已发送显示回执:", this.notification.eventId);
|
console.log("已发送显示回执:", this.currentNotification.eventId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("发送显示回执失败:", error);
|
console.warn("发送显示回执失败:", error);
|
||||||
@ -244,34 +350,64 @@ export default {
|
|||||||
// 发送已读回执
|
// 发送已读回执
|
||||||
sendReadReceipt() {
|
sendReadReceipt() {
|
||||||
try {
|
try {
|
||||||
if (this.$refs.eventSender && this.notification?.eventId) {
|
if (this.$refs.eventSender && this.currentNotification?.eventId) {
|
||||||
this.$refs.eventSender.sendReadReceipt(
|
this.$refs.eventSender.sendReadReceipt(
|
||||||
{},
|
{},
|
||||||
this.notification.content.notificationId
|
this.currentNotification.content.notificationId
|
||||||
);
|
);
|
||||||
console.log("已发送已读回执:", this.notification.eventId);
|
console.log("已发送已读回执:", this.currentNotification.eventId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("发送已读回执失败:", error);
|
console.warn("发送已读回执失败:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 导航到上一个通知
|
||||||
|
previousNotification() {
|
||||||
|
if (this.currentIndex > 0) {
|
||||||
|
this.currentIndex--;
|
||||||
|
this.sendDisplayedReceipt();
|
||||||
|
|
||||||
|
// 根据新通知的紧急程度调整音效
|
||||||
|
if (this.isUrgent) {
|
||||||
|
this.startUrgentSound();
|
||||||
|
} else {
|
||||||
|
this.stopUrgentSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 导航到下一个通知
|
||||||
|
nextNotification() {
|
||||||
|
if (this.currentIndex < this.notificationQueue.length - 1) {
|
||||||
|
this.currentIndex++;
|
||||||
|
this.sendDisplayedReceipt();
|
||||||
|
|
||||||
|
// 根据新通知的紧急程度调整音效
|
||||||
|
if (this.isUrgent) {
|
||||||
|
this.startUrgentSound();
|
||||||
|
} else {
|
||||||
|
this.stopUrgentSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// 启动加急通知的定时音效
|
// 启动加急通知的定时音效
|
||||||
startUrgentSound() {
|
startUrgentSound() {
|
||||||
// 清除之前的定时器
|
this.stopUrgentSound(); // 先清除之前的定时器
|
||||||
if (this.urgentSoundTimer) {
|
|
||||||
clearInterval(this.urgentSoundTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每秒播放一次提示音
|
// 每秒播放一次提示音
|
||||||
this.urgentSoundTimer = setInterval(() => {
|
this.urgentSoundTimer = setInterval(() => {
|
||||||
if (this.visible && this.isUrgent) {
|
if (this.visible && this.isUrgent) {
|
||||||
this.playNotificationSound();
|
this.playNotificationSound();
|
||||||
} else {
|
} else {
|
||||||
// 如果弹框已关闭或不再是加急状态,停止音效
|
this.stopUrgentSound();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
// 停止加急音效
|
||||||
|
stopUrgentSound() {
|
||||||
|
if (this.urgentSoundTimer) {
|
||||||
clearInterval(this.urgentSoundTimer);
|
clearInterval(this.urgentSoundTimer);
|
||||||
this.urgentSoundTimer = null;
|
this.urgentSoundTimer = null;
|
||||||
}
|
}
|
||||||
}, 1000);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -341,6 +477,21 @@ export default {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-controls {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-counter {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 动画效果 */
|
/* 动画效果 */
|
||||||
@keyframes urgentPulse {
|
@keyframes urgentPulse {
|
||||||
0%,
|
0%,
|
||||||
|
|||||||
430
src/components/UrgentTestDialog.vue
Normal file
430
src/components/UrgentTestDialog.vue
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
fullscreen
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar
|
||||||
|
dark
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<v-toolbar-title>
|
||||||
|
<v-icon class="mr-2">
|
||||||
|
mdi-chat
|
||||||
|
</v-icon>
|
||||||
|
发送通知
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-card-text class="pa-0">
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-switch
|
||||||
|
v-model="notificationForm.isUrgent"
|
||||||
|
label="强调通知"
|
||||||
|
color="red"
|
||||||
|
inset
|
||||||
|
>
|
||||||
|
|
||||||
|
</v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="notificationForm.message"
|
||||||
|
label="通知内容"
|
||||||
|
outlined
|
||||||
|
rows="3"
|
||||||
|
placeholder="请输入强调通知的内容..."
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="px-6 pb-6">
|
||||||
|
<v-btn
|
||||||
|
:color="notificationForm.isUrgent ? 'red' : 'blue'"
|
||||||
|
:disabled="!notificationForm.message.trim()"
|
||||||
|
:loading="sending"
|
||||||
|
size="large"
|
||||||
|
variant="elevated"
|
||||||
|
@click="sendNotification"
|
||||||
|
>
|
||||||
|
<v-icon left>
|
||||||
|
{{ notificationForm.isUrgent ? 'mdi-alert-circle' : 'mdi-information' }}
|
||||||
|
</v-icon>
|
||||||
|
{{ notificationForm.isUrgent ? '发送强调通知' : '发送通知' }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 消息发送历史 -->
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon class="mr-2">
|
||||||
|
mdi-history
|
||||||
|
</v-icon>
|
||||||
|
消息记录
|
||||||
|
<v-spacer />
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div
|
||||||
|
v-if="sentMessages.length === 0"
|
||||||
|
class="text-center text-grey py-8"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
size="64"
|
||||||
|
color="grey-lighten-2"
|
||||||
|
>
|
||||||
|
mdi-message-outline
|
||||||
|
</v-icon>
|
||||||
|
<div class="mt-2">
|
||||||
|
暂无发送记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col
|
||||||
|
v-for="message in sentMessages.slice().reverse()"
|
||||||
|
:key="message.id"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
>
|
||||||
|
<!-- 主消息卡片 -->
|
||||||
|
<v-card
|
||||||
|
:color="getMainCardColor(message.receipts)"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<span class="font-weight-medium">
|
||||||
|
{{ message.isUrgent ? '强调通知' : '通知' }}
|
||||||
|
</span>
|
||||||
|
<v-spacer />
|
||||||
|
<span class="text-caption font-weight-medium">
|
||||||
|
{{ getReceiptStatus(message.receipts) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-body-2 mb-3"
|
||||||
|
style="max-height: 60px; overflow: hidden;"
|
||||||
|
>
|
||||||
|
{{ message.message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-caption">
|
||||||
|
<div>发送时间:{{ formatTime(message.timestamp) }}</div>
|
||||||
|
<div>事件ID:{{ message.id }}</div>
|
||||||
|
<div>通知ID:{{ message.notificationId }}</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 设备回执小卡片 -->
|
||||||
|
<div v-if="hasAnyReceipts(message.receipts)">
|
||||||
|
<!-- 已读设备 -->
|
||||||
|
<v-card
|
||||||
|
v-for="device in message.receipts.read"
|
||||||
|
:key="`${device.senderId}-read`"
|
||||||
|
color="success"
|
||||||
|
class="mb-1"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-2">
|
||||||
|
<div class="align-center">
|
||||||
|
|
||||||
|
<span class="text-body-2 font-weight-medium">{{ device.deviceName }} </span>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{ device.deviceType }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="text-caption mt-1">
|
||||||
|
已读于 {{ formatDeviceTime(device.timestamp) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 已显示设备(排除已读的设备) -->
|
||||||
|
<v-card
|
||||||
|
v-for="device in getDisplayedOnlyDevices(message.receipts)"
|
||||||
|
:key="`${device.senderId}-displayed`"
|
||||||
|
color="info-lighten-4"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-1"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-2">
|
||||||
|
<div class="align-center">
|
||||||
|
|
||||||
|
<span class="text-body-2 font-weight-medium">{{ device.deviceName }}</span>
|
||||||
|
<v-spacer />
|
||||||
|
<span class="text-caption text-grey">
|
||||||
|
{{ device.deviceType=="classroom"?"教室设备上的应用":device.deviceType }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey mt-1">
|
||||||
|
已显示于 {{ formatDeviceTime(device.timestamp) }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div v-else> <v-card
|
||||||
|
|
||||||
|
color="info-lighten-4"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-1"
|
||||||
|
size="small"
|
||||||
|
title="无设备在线"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
如果数秒后任然显示这个提示,则可能没有任何设备在线接收通知。
|
||||||
|
</v-card-text>
|
||||||
|
</v-card></div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<ChatWidget />
|
||||||
|
<EventSender ref="eventSender" />
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChatWidget from '@/components/ChatWidget.vue'
|
||||||
|
import EventSender from '@/components/EventSender.vue'
|
||||||
|
import { on as socketOn } from '@/utils/socketClient'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UrgentTestDialog',
|
||||||
|
components: {
|
||||||
|
ChatWidget,
|
||||||
|
EventSender
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
sending: false,
|
||||||
|
notificationForm: {
|
||||||
|
isUrgent: false,
|
||||||
|
message: ''
|
||||||
|
},
|
||||||
|
sentMessages: [],
|
||||||
|
receiptCleanup: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialog: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setupEventListeners()
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.cleanup()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generateNotificationId() {
|
||||||
|
// 生成32位随机字符串
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let result = ''
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendNotification() {
|
||||||
|
if (!this.notificationForm.message.trim()) return
|
||||||
|
|
||||||
|
this.sending = true
|
||||||
|
try {
|
||||||
|
// 生成32位随机通知ID
|
||||||
|
const notificationId = this.generateNotificationId()
|
||||||
|
|
||||||
|
const result = await this.$refs.eventSender.sendNotification(
|
||||||
|
this.notificationForm.message,
|
||||||
|
this.notificationForm.isUrgent,
|
||||||
|
[],
|
||||||
|
{ deviceName: '测试设备', deviceType: 'system', isReadOnly: false },
|
||||||
|
notificationId
|
||||||
|
)
|
||||||
|
|
||||||
|
const eventId = result?.eventId || `msg-${Date.now()}`
|
||||||
|
|
||||||
|
this.sentMessages.push({
|
||||||
|
id: eventId,
|
||||||
|
notificationId: notificationId,
|
||||||
|
message: this.notificationForm.message,
|
||||||
|
isUrgent: this.notificationForm.isUrgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
receipts: {
|
||||||
|
displayed: [],
|
||||||
|
read: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('通知已发送,事件ID:', eventId, '通知ID:', notificationId)
|
||||||
|
this.resetForm()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送通知失败:', error)
|
||||||
|
} finally {
|
||||||
|
this.sending = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.dialog = false
|
||||||
|
},
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// 监听显示回执
|
||||||
|
const cleanup1 = socketOn('notification-displayed', (data) => {
|
||||||
|
console.log('收到显示回执:', data)
|
||||||
|
this.updateReceipt(data, 'displayed')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听已读回执
|
||||||
|
const cleanup2 = socketOn('notification-read', (data) => {
|
||||||
|
console.log('收到已读回执:', data)
|
||||||
|
this.updateReceipt(data, 'read')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.receiptCleanup.push(cleanup1, cleanup2)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReceipt(data, type) {
|
||||||
|
const originalEventId = data.originalEventId
|
||||||
|
const notificationId = data.notificationId || data.content?.notificationId
|
||||||
|
|
||||||
|
if (!originalEventId && !notificationId) return
|
||||||
|
|
||||||
|
const message = this.sentMessages.find(msg =>
|
||||||
|
msg.id === originalEventId ||
|
||||||
|
msg.notificationId === notificationId
|
||||||
|
)
|
||||||
|
if (message) {
|
||||||
|
// 使用 senderInfo 中的设备信息,并包含 senderId
|
||||||
|
const deviceInfo = {
|
||||||
|
senderId: data.senderId || 'unknown-sender',
|
||||||
|
deviceName: data.senderInfo?.deviceName || data.deviceInfo?.deviceName || '未知设备',
|
||||||
|
deviceType: data.senderInfo?.deviceType || data.deviceInfo?.deviceType || 'unknown',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 避免重复添加相同设备(按 senderId 判断)
|
||||||
|
const exists = message.receipts[type].find(item =>
|
||||||
|
item.senderId === deviceInfo.senderId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
message.receipts[type].push(deviceInfo)
|
||||||
|
console.log(`更新${type}回执:`, message.id, deviceInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.receiptCleanup.forEach(cleanup => cleanup())
|
||||||
|
this.receiptCleanup = []
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(timestamp) {
|
||||||
|
return new Date(timestamp).toLocaleString('zh-CN')
|
||||||
|
},
|
||||||
|
|
||||||
|
getReceiptStatus(receipts) {
|
||||||
|
if (receipts.read.length > 0) return '已读'
|
||||||
|
if (receipts.displayed.length > 0) return '已显示'
|
||||||
|
return '已发送'
|
||||||
|
},
|
||||||
|
|
||||||
|
getReceiptColor(receipts) {
|
||||||
|
if (receipts.read.length > 0) return 'success'
|
||||||
|
if (receipts.displayed.length > 0) return 'info'
|
||||||
|
return 'grey'
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDeviceTime(timestamp) {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('zh-CN')
|
||||||
|
},
|
||||||
|
|
||||||
|
getMainCardColor(receipts) {
|
||||||
|
// 优先显示已读状态(绿色),其次是已显示状态(蓝色)
|
||||||
|
if (receipts.read.length > 0) return 'success'
|
||||||
|
if (receipts.displayed.length > 0) return 'info'
|
||||||
|
return 'grey'
|
||||||
|
},
|
||||||
|
|
||||||
|
hasAnyReceipts(receipts) {
|
||||||
|
return receipts.read.length > 0 || receipts.displayed.length > 0
|
||||||
|
},
|
||||||
|
|
||||||
|
getDisplayedOnlyDevices(receipts) {
|
||||||
|
// 返回只显示未读的设备(按 senderId 排除已读的设备)
|
||||||
|
const readSenderIds = receipts.read.map(device => device.senderId)
|
||||||
|
return receipts.displayed.filter(device =>
|
||||||
|
!readSenderIds.includes(device.senderId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gap-1 {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-history-card .v-chip {
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -31,6 +31,13 @@
|
|||||||
{{ tokenDisplayInfo.text }}
|
{{ tokenDisplayInfo.text }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="shouldShowUrgentTestButton"
|
||||||
|
prepend-icon="mdi-chat"
|
||||||
|
@click="urgentTestDialog = true"
|
||||||
|
variant="tonal"
|
||||||
|
>发送通知</v-btn
|
||||||
|
>
|
||||||
<v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true" />
|
<v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true" />
|
||||||
<v-btn
|
<v-btn
|
||||||
:badge="unreadCount || undefined"
|
:badge="unreadCount || undefined"
|
||||||
@ -221,7 +228,13 @@
|
|||||||
<!-- 出勤统计区域 -->
|
<!-- 出勤统计区域 -->
|
||||||
<v-col
|
<v-col
|
||||||
v-if="state.studentList && state.studentList.length"
|
v-if="state.studentList && state.studentList.length"
|
||||||
v-ripple="{ class: `text-${['primary','secondary','info','success','warning','error'][Math.floor(Math.random()*6)]}` }"
|
v-ripple="{
|
||||||
|
class: `text-${
|
||||||
|
['primary', 'secondary', 'info', 'success', 'warning', 'error'][
|
||||||
|
Math.floor(Math.random() * 6)
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
}"
|
||||||
class="attendance-area no-select"
|
class="attendance-area no-select"
|
||||||
cols="1"
|
cols="1"
|
||||||
@click="setAttendanceArea()"
|
@click="setAttendanceArea()"
|
||||||
@ -450,8 +463,11 @@
|
|||||||
class="mr-2"
|
class="mr-2"
|
||||||
size="24"
|
size="24"
|
||||||
>
|
>
|
||||||
<v-icon size="small">{{
|
<v-icon size="small"
|
||||||
getStudentStatusIcon(state.studentList.indexOf(student))
|
>{{
|
||||||
|
getStudentStatusIcon(
|
||||||
|
state.studentList.indexOf(student)
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
@ -552,8 +568,7 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row
|
</v-row>
|
||||||
>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
@ -592,6 +607,9 @@
|
|||||||
<!-- 设备聊天室(右下角浮窗) -->
|
<!-- 设备聊天室(右下角浮窗) -->
|
||||||
<ChatWidget v-model="isChatOpen" :show-button="false" />
|
<ChatWidget v-model="isChatOpen" :show-button="false" />
|
||||||
|
|
||||||
|
<!-- 紧急通知测试对话框 -->
|
||||||
|
<UrgentTestDialog v-model="urgentTestDialog" />
|
||||||
|
|
||||||
<!-- 添加确认对话框 -->
|
<!-- 添加确认对话框 -->
|
||||||
<v-dialog v-model="confirmDialog.show" max-width="400">
|
<v-dialog v-model="confirmDialog.show" max-width="400">
|
||||||
<v-card>
|
<v-card>
|
||||||
@ -632,9 +650,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-list-item-title class="d-flex align-center">
|
<v-list-item-title class="d-flex align-center">
|
||||||
<span class="text-subtitle-1">{{ change.name }}</span>
|
<span class="text-subtitle-1">{{ change.name }}</span>
|
||||||
<v-tooltip activator="parent" location="top">{{
|
<v-tooltip activator="parent" location="top"
|
||||||
change.description || change.key
|
>{{ change.description || change.key }}
|
||||||
}}
|
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
@ -661,8 +678,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog
|
</v-dialog>
|
||||||
>
|
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -675,6 +691,7 @@ import ChatWidget from "@/components/ChatWidget.vue";
|
|||||||
import HomeworkEditDialog from "@/components/HomeworkEditDialog.vue";
|
import HomeworkEditDialog from "@/components/HomeworkEditDialog.vue";
|
||||||
import InitServiceChooser from "@/components/InitServiceChooser.vue";
|
import InitServiceChooser from "@/components/InitServiceChooser.vue";
|
||||||
import StudentNameManager from "@/components/StudentNameManager.vue";
|
import StudentNameManager from "@/components/StudentNameManager.vue";
|
||||||
|
import UrgentTestDialog from "@/components/UrgentTestDialog.vue";
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider from "@/utils/dataProvider";
|
||||||
import {
|
import {
|
||||||
getSetting,
|
getSetting,
|
||||||
@ -698,6 +715,7 @@ import {
|
|||||||
onConnect as onSocketConnect,
|
onConnect as onSocketConnect,
|
||||||
} from "@/utils/socketClient";
|
} from "@/utils/socketClient";
|
||||||
import { createDeviceEventHandler } from "@/utils/deviceEvents";
|
import { createDeviceEventHandler } from "@/utils/deviceEvents";
|
||||||
|
import axios from "@/axios/axios";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Classworks 作业板",
|
name: "Classworks 作业板",
|
||||||
@ -710,6 +728,7 @@ export default {
|
|||||||
InitServiceChooser,
|
InitServiceChooser,
|
||||||
ChatWidget,
|
ChatWidget,
|
||||||
StudentNameManager,
|
StudentNameManager,
|
||||||
|
UrgentTestDialog,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const defaultSubjects = [
|
const defaultSubjects = [
|
||||||
@ -800,11 +819,11 @@ export default {
|
|||||||
tokenDisplayInfo: {
|
tokenDisplayInfo: {
|
||||||
show: false,
|
show: false,
|
||||||
readonly: false, // 是否是只读 token
|
readonly: false, // 是否是只读 token
|
||||||
text: '',
|
text: "",
|
||||||
color: 'primary',
|
color: "primary",
|
||||||
variant: 'tonal',
|
variant: "tonal",
|
||||||
icon: 'mdi-account',
|
icon: "mdi-account",
|
||||||
disabled: false
|
disabled: false,
|
||||||
},
|
},
|
||||||
// 实时刷新信息
|
// 实时刷新信息
|
||||||
realtimeInfo: {
|
realtimeInfo: {
|
||||||
@ -820,8 +839,12 @@ export default {
|
|||||||
namespace: null,
|
namespace: null,
|
||||||
authCode: null,
|
authCode: null,
|
||||||
autoOpen: false,
|
autoOpen: false,
|
||||||
autoExecute: false
|
autoExecute: false,
|
||||||
},
|
},
|
||||||
|
// 紧急通知测试对话框
|
||||||
|
urgentTestDialog: false,
|
||||||
|
// 令牌信息
|
||||||
|
tokenInfo: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -970,6 +993,26 @@ export default {
|
|||||||
void this.settingsTick;
|
void this.settingsTick;
|
||||||
return onHome && isKv && (!token || token === "");
|
return onHome && isKv && (!token || token === "");
|
||||||
},
|
},
|
||||||
|
// 是否显示紧急通知测试按钮(仅教师和课堂令牌)
|
||||||
|
shouldShowUrgentTestButton() {
|
||||||
|
// 检查是否使用 KV 服务器
|
||||||
|
const provider = getSetting("server.provider");
|
||||||
|
const isKv = provider === "kv-server" || provider === "classworkscloud";
|
||||||
|
if (!isKv) return false;
|
||||||
|
|
||||||
|
// 检查是否有令牌
|
||||||
|
const kvToken = getSetting("server.kvToken");
|
||||||
|
if (!kvToken) return false;
|
||||||
|
|
||||||
|
// 检查令牌信息是否已加载
|
||||||
|
if (!this.tokenInfo) return false;
|
||||||
|
|
||||||
|
// 只有 teacher 或 classroom 类型的令牌才显示
|
||||||
|
return (
|
||||||
|
this.tokenInfo.deviceType === "teacher" ||
|
||||||
|
this.tokenInfo.deviceType === "classroom"
|
||||||
|
);
|
||||||
|
},
|
||||||
filteredStudents() {
|
filteredStudents() {
|
||||||
let students = [...this.state.studentList];
|
let students = [...this.state.studentList];
|
||||||
|
|
||||||
@ -1081,15 +1124,22 @@ export default {
|
|||||||
if (studentNameManager) {
|
if (studentNameManager) {
|
||||||
this.studentNameInfo.name = studentNameManager.currentStudentName;
|
this.studentNameInfo.name = studentNameManager.currentStudentName;
|
||||||
this.studentNameInfo.isStudent = studentNameManager.isStudentToken;
|
this.studentNameInfo.isStudent = studentNameManager.isStudentToken;
|
||||||
this.studentNameInfo.openDialog = () => studentNameManager.openDialog();
|
this.studentNameInfo.openDialog = () =>
|
||||||
|
studentNameManager.openDialog();
|
||||||
|
|
||||||
// 监听学生姓名变化
|
// 监听学生姓名变化
|
||||||
this.$watch(() => studentNameManager.currentStudentName, (newName) => {
|
this.$watch(
|
||||||
|
() => studentNameManager.currentStudentName,
|
||||||
|
(newName) => {
|
||||||
this.studentNameInfo.name = newName;
|
this.studentNameInfo.name = newName;
|
||||||
});
|
}
|
||||||
this.$watch(() => studentNameManager.isStudentToken, (isStudent) => {
|
);
|
||||||
|
this.$watch(
|
||||||
|
() => studentNameManager.isStudentToken,
|
||||||
|
(isStudent) => {
|
||||||
this.studentNameInfo.isStudent = isStudent;
|
this.studentNameInfo.isStudent = isStudent;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1121,6 +1171,9 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.updateTokenDisplayInfo();
|
this.updateTokenDisplayInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取令牌信息
|
||||||
|
await this.loadTokenInfo();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("初始化失败:", err);
|
console.error("初始化失败:", err);
|
||||||
this.showError("初始化失败,请刷新页面重试");
|
this.showError("初始化失败,请刷新页面重试");
|
||||||
@ -1156,21 +1209,21 @@ export default {
|
|||||||
|
|
||||||
// 退出设备房间并清理监听
|
// 退出设备房间并清理监听
|
||||||
try {
|
try {
|
||||||
if (this.$offKvChanged && typeof this.$offKvChanged === 'function') {
|
if (this.$offKvChanged && typeof this.$offKvChanged === "function") {
|
||||||
this.$offKvChanged();
|
this.$offKvChanged();
|
||||||
this.$offKvChanged = null;
|
this.$offKvChanged = null;
|
||||||
}
|
}
|
||||||
if (this.$offDeviceEvent && typeof this.$offDeviceEvent === 'function') {
|
if (this.$offDeviceEvent && typeof this.$offDeviceEvent === "function") {
|
||||||
this.$offDeviceEvent();
|
this.$offDeviceEvent();
|
||||||
this.$offDeviceEvent = null;
|
this.$offDeviceEvent = null;
|
||||||
}
|
}
|
||||||
if (this.$offConnect && typeof this.$offConnect === 'function') {
|
if (this.$offConnect && typeof this.$offConnect === "function") {
|
||||||
this.$offConnect();
|
this.$offConnect();
|
||||||
this.$offConnect = null;
|
this.$offConnect = null;
|
||||||
}
|
}
|
||||||
leaveAll();
|
leaveAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('主页面事件清理失败:', e);
|
console.warn("主页面事件清理失败:", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1179,7 +1232,8 @@ export default {
|
|||||||
async loadDeviceInfo() {
|
async loadDeviceInfo() {
|
||||||
try {
|
try {
|
||||||
const provider = getSetting("server.provider");
|
const provider = getSetting("server.provider");
|
||||||
const useServer = provider === "kv-server" || provider === "classworkscloud";
|
const useServer =
|
||||||
|
provider === "kv-server" || provider === "classworkscloud";
|
||||||
if (!useServer) return;
|
if (!useServer) return;
|
||||||
|
|
||||||
const res = await kvServerProvider.loadNamespaceInfo();
|
const res = await kvServerProvider.loadNamespaceInfo();
|
||||||
@ -1187,56 +1241,82 @@ export default {
|
|||||||
|
|
||||||
this.state.namespaceInfo = res || null;
|
this.state.namespaceInfo = res || null;
|
||||||
// 兜底填充设备名,避免重复解析
|
// 兜底填充设备名,避免重复解析
|
||||||
this.state.deviceName =
|
this.state.deviceName = res?.account?.deviceName || "";
|
||||||
res?.account?.deviceName ||
|
|
||||||
"";
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("加载设备信息失败:", e);
|
console.warn("加载设备信息失败:", e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取令牌信息
|
||||||
|
async loadTokenInfo() {
|
||||||
|
try {
|
||||||
|
const provider = getSetting("server.provider");
|
||||||
|
const isKv = provider === "kv-server" || provider === "classworkscloud";
|
||||||
|
if (!isKv) return;
|
||||||
|
|
||||||
|
const kvToken = getSetting("server.kvToken");
|
||||||
|
if (!kvToken) return;
|
||||||
|
|
||||||
|
const serverUrl = getSetting("server.domain");
|
||||||
|
if (!serverUrl) return;
|
||||||
|
|
||||||
|
// 获取 Token 信息
|
||||||
|
const tokenResponse = await axios.get(`${serverUrl}/kv/_token`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${kvToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tokenInfo = tokenResponse.data;
|
||||||
|
console.log("Token info loaded:", this.tokenInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load token info:", error);
|
||||||
|
this.tokenInfo = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 更新 Token 显示信息
|
// 更新 Token 显示信息
|
||||||
updateTokenDisplayInfo() {
|
updateTokenDisplayInfo() {
|
||||||
const manager = this.$refs.studentNameManager
|
const manager = this.$refs.studentNameManager;
|
||||||
if (!manager || !manager.hasToken) {
|
if (!manager || !manager.hasToken) {
|
||||||
this.tokenDisplayInfo.show = false
|
this.tokenDisplayInfo.show = false;
|
||||||
this.tokenDisplayInfo.readonly = false
|
this.tokenDisplayInfo.readonly = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = manager.displayName
|
const displayName = manager.displayName;
|
||||||
const isReadOnly = manager.isReadOnly
|
const isReadOnly = manager.isReadOnly;
|
||||||
const isStudent = manager.isStudentToken
|
const isStudent = manager.isStudentToken;
|
||||||
|
|
||||||
// 设置只读状态(对所有类型的 token 都显示)
|
// 设置只读状态(对所有类型的 token 都显示)
|
||||||
this.tokenDisplayInfo.readonly = isReadOnly
|
this.tokenDisplayInfo.readonly = isReadOnly;
|
||||||
|
|
||||||
// 只有学生类型的 token 才显示名称 chip
|
// 只有学生类型的 token 才显示名称 chip
|
||||||
if (!isStudent) {
|
if (!isStudent) {
|
||||||
this.tokenDisplayInfo.show = false
|
this.tokenDisplayInfo.show = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置学生名称显示(始终蓝色)
|
// 设置学生名称显示(始终蓝色)
|
||||||
this.tokenDisplayInfo.text = displayName
|
this.tokenDisplayInfo.text = displayName;
|
||||||
this.tokenDisplayInfo.color = 'primary'
|
this.tokenDisplayInfo.color = "primary";
|
||||||
this.tokenDisplayInfo.icon = 'mdi-account'
|
this.tokenDisplayInfo.icon = "mdi-account";
|
||||||
this.tokenDisplayInfo.disabled = isReadOnly // 只读时不可点击
|
this.tokenDisplayInfo.disabled = isReadOnly; // 只读时不可点击
|
||||||
this.tokenDisplayInfo.show = true
|
this.tokenDisplayInfo.show = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理 Token Chip 点击
|
// 处理 Token Chip 点击
|
||||||
handleTokenChipClick() {
|
handleTokenChipClick() {
|
||||||
console.log('Token chip clicked')
|
console.log("Token chip clicked");
|
||||||
const manager = this.$refs.studentNameManager
|
const manager = this.$refs.studentNameManager;
|
||||||
console.log('Manager:', manager)
|
console.log("Manager:", manager);
|
||||||
console.log('Is student token:', manager?.isStudentToken)
|
console.log("Is student token:", manager?.isStudentToken);
|
||||||
|
|
||||||
if (manager && manager.isStudentToken) {
|
if (manager && manager.isStudentToken) {
|
||||||
console.log('Opening dialog...')
|
console.log("Opening dialog...");
|
||||||
manager.openDialog()
|
manager.openDialog();
|
||||||
} else {
|
} else {
|
||||||
console.log('Cannot open dialog - conditions not met')
|
console.log("Cannot open dialog - conditions not met");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1318,7 +1398,12 @@ export default {
|
|||||||
this.state.showNoDataMessage = true;
|
this.state.showNoDataMessage = true;
|
||||||
this.state.noDataMessage = response.error.message;
|
this.state.noDataMessage = response.error.message;
|
||||||
// 如果强制清空或当前没有数据时才设置为空
|
// 如果强制清空或当前没有数据时才设置为空
|
||||||
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
|
if (
|
||||||
|
forceClear ||
|
||||||
|
!this.state.boardData ||
|
||||||
|
(!this.state.boardData.homework &&
|
||||||
|
!this.state.boardData.attendance)
|
||||||
|
) {
|
||||||
this.state.boardData = {
|
this.state.boardData = {
|
||||||
homework: {},
|
homework: {},
|
||||||
attendance: { absent: [], late: [], exclude: [] },
|
attendance: { absent: [], late: [], exclude: [] },
|
||||||
@ -1345,7 +1430,11 @@ export default {
|
|||||||
console.error("数据加载失败:", error);
|
console.error("数据加载失败:", error);
|
||||||
this.$message.error("下载失败", error.message);
|
this.$message.error("下载失败", error.message);
|
||||||
// 如果强制清空或当前没有任何数据,才初始化为空数据
|
// 如果强制清空或当前没有任何数据,才初始化为空数据
|
||||||
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
|
if (
|
||||||
|
forceClear ||
|
||||||
|
!this.state.boardData ||
|
||||||
|
(!this.state.boardData.homework && !this.state.boardData.attendance)
|
||||||
|
) {
|
||||||
this.state.boardData = {
|
this.state.boardData = {
|
||||||
homework: {},
|
homework: {},
|
||||||
attendance: { absent: [], late: [], exclude: [] },
|
attendance: { absent: [], late: [], exclude: [] },
|
||||||
@ -1612,6 +1701,8 @@ export default {
|
|||||||
this.updateBackendUrl();
|
this.updateBackendUrl();
|
||||||
// 设置更新时尝试刷新设备名称(例如 Token 或域名变更)
|
// 设置更新时尝试刷新设备名称(例如 Token 或域名变更)
|
||||||
this.loadDeviceInfo();
|
this.loadDeviceInfo();
|
||||||
|
// 重新加载令牌信息(Token 可能已变更)
|
||||||
|
this.loadTokenInfo();
|
||||||
// 触发依赖刷新(例如 shouldShowInit)
|
// 触发依赖刷新(例如 shouldShowInit)
|
||||||
this.settingsTick++;
|
this.settingsTick++;
|
||||||
},
|
},
|
||||||
@ -1634,8 +1725,7 @@ export default {
|
|||||||
.replace({
|
.replace({
|
||||||
query: { date: formattedDate },
|
query: { date: formattedDate },
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {});
|
||||||
});
|
|
||||||
|
|
||||||
// Load both data and subjects in parallel, force clear data when switching dates
|
// Load both data and subjects in parallel, force clear data when switching dates
|
||||||
await Promise.all([this.downloadData(true), this.loadSubjects()]);
|
await Promise.all([this.downloadData(true), this.loadSubjects()]);
|
||||||
@ -1748,13 +1838,13 @@ export default {
|
|||||||
// 新格式:直接事件数据
|
// 新格式:直接事件数据
|
||||||
if (eventData.content && eventData.timestamp) {
|
if (eventData.content && eventData.timestamp) {
|
||||||
msg = {
|
msg = {
|
||||||
uuid: eventData.senderId || 'realtime',
|
uuid: eventData.senderId || "realtime",
|
||||||
key: eventData.content.key,
|
key: eventData.content.key,
|
||||||
action: eventData.content.action,
|
action: eventData.content.action,
|
||||||
created: eventData.content.created,
|
created: eventData.content.created,
|
||||||
updatedAt: eventData.content.updatedAt || eventData.timestamp,
|
updatedAt: eventData.content.updatedAt || eventData.timestamp,
|
||||||
deletedAt: eventData.content.deletedAt,
|
deletedAt: eventData.content.deletedAt,
|
||||||
batch: eventData.content.batch
|
batch: eventData.content.batch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1766,9 +1856,12 @@ export default {
|
|||||||
// 保留设备事件监听(为未来扩展)
|
// 保留设备事件监听(为未来扩展)
|
||||||
this.deviceEventHandler = createDeviceEventHandler({
|
this.deviceEventHandler = createDeviceEventHandler({
|
||||||
onKvChanged: handler,
|
onKvChanged: handler,
|
||||||
enableLegacySupport: true
|
enableLegacySupport: true,
|
||||||
});
|
});
|
||||||
this.$offDeviceEvent = socketOn("device-event", this.deviceEventHandler);
|
this.$offDeviceEvent = socketOn(
|
||||||
|
"device-event",
|
||||||
|
this.deviceEventHandler
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("实时频道初始化失败", e);
|
console.warn("实时频道初始化失败", e);
|
||||||
}
|
}
|
||||||
@ -2334,8 +2427,10 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const namespace = urlParams.get("namespace");
|
const namespace = urlParams.get("namespace");
|
||||||
const authCode = urlParams.get("authCode") || urlParams.get("auth_code");
|
const authCode =
|
||||||
const autoExecute = urlParams.get("autoExecute") || urlParams.get("auto_execute");
|
urlParams.get("authCode") || urlParams.get("auth_code");
|
||||||
|
const autoExecute =
|
||||||
|
urlParams.get("autoExecute") || urlParams.get("auto_execute");
|
||||||
|
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
this.preconfigData.namespace = namespace;
|
this.preconfigData.namespace = namespace;
|
||||||
@ -2347,11 +2442,17 @@ export default {
|
|||||||
console.log("检测到预配数据:", {
|
console.log("检测到预配数据:", {
|
||||||
namespace: this.preconfigData.namespace,
|
namespace: this.preconfigData.namespace,
|
||||||
hasAuthCode: !!this.preconfigData.authCode,
|
hasAuthCode: !!this.preconfigData.authCode,
|
||||||
autoExecute: this.preconfigData.autoExecute
|
autoExecute: this.preconfigData.autoExecute,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清理URL参数,避免重复处理
|
// 清理URL参数,避免重复处理
|
||||||
this.cleanupUrlParams(['namespace', 'authCode', 'auth_code', 'autoExecute', 'auto_execute']);
|
this.cleanupUrlParams([
|
||||||
|
"namespace",
|
||||||
|
"authCode",
|
||||||
|
"auth_code",
|
||||||
|
"autoExecute",
|
||||||
|
"auto_execute",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("解析预配数据失败:", error);
|
console.error("解析预配数据失败:", error);
|
||||||
@ -2362,7 +2463,9 @@ export default {
|
|||||||
parseBoolean(value) {
|
parseBoolean(value) {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
const lowerValue = value.toLowerCase();
|
const lowerValue = value.toLowerCase();
|
||||||
return lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
|
return (
|
||||||
|
lowerValue === "true" || lowerValue === "1" || lowerValue === "yes"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清理URL参数
|
// 清理URL参数
|
||||||
@ -2371,7 +2474,7 @@ export default {
|
|||||||
const url = new URL(window.location);
|
const url = new URL(window.location);
|
||||||
let hasChanged = false;
|
let hasChanged = false;
|
||||||
|
|
||||||
params.forEach(param => {
|
params.forEach((param) => {
|
||||||
if (url.searchParams.has(param)) {
|
if (url.searchParams.has(param)) {
|
||||||
url.searchParams.delete(param);
|
url.searchParams.delete(param);
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user