From 46dffb02ca516cdc69102cedbf87aaa051f11e04 Mon Sep 17 00:00:00 2001 From: Sunwuyuan Date: Sun, 30 Nov 2025 11:50:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B8=B8=E9=A9=BB?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BC=96=E8=BE=91=E5=92=8C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/HomeworkEditDialog.vue | 28 ++ src/components/UrgentTestDialog.vue | 292 +++++++++++++++++- .../attendance/AttendanceSidebar.vue | 22 +- src/components/home/HomeworkGrid.vue | 49 ++- src/pages/index.vue | 192 +++++++++++- src/utils/settings.js | 7 + 6 files changed, 564 insertions(+), 26 deletions(-) diff --git a/src/components/HomeworkEditDialog.vue b/src/components/HomeworkEditDialog.vue index ca643a2..c762f67 100644 --- a/src/components/HomeworkEditDialog.vue +++ b/src/components/HomeworkEditDialog.vue @@ -213,6 +213,26 @@ + + + +
+
你打算修改历史?
+
+ 这是 {{ new Date(currentDateString.slice(0,4), currentDateString.slice(4,6)-1, currentDateString.slice(6,8)).toLocaleDateString() }} 的作业 • 请谨慎操作,确保不会覆盖重要数据 +
+
+
+
点击空白处完成编辑
@@ -244,6 +264,14 @@ export default { autoSave: { type: Boolean, default: false + }, + isEditingPastData: { + type: Boolean, + default: false + }, + currentDateString: { + type: String, + default: "" } }, emits: ["update:modelValue", "save"], diff --git a/src/components/UrgentTestDialog.vue b/src/components/UrgentTestDialog.vue index 1e3969c..e3f4dfe 100644 --- a/src/components/UrgentTestDialog.vue +++ b/src/components/UrgentTestDialog.vue @@ -43,8 +43,14 @@ color="red" inset > - + + + + + + + mdi-pin + 常驻通知管理 + + +
+ 暂无常驻通知 +
+ + + + + + +
+
+
+
+ @@ -222,6 +264,57 @@ + + + + + + 编辑常驻通知 + + + + + + + + + + + + + 取消 + 保存 + + + + + + + + 确认删除 + 确定要删除这条常驻通知吗?此操作无法撤销。 + + + 取消 + 删除 + + + @@ -229,6 +322,7 @@ import ChatWidget from '@/components/ChatWidget.vue' import EventSender from '@/components/EventSender.vue' import { on as socketOn } from '@/utils/socketClient' +import dataProvider from '@/utils/dataProvider' export default { name: 'UrgentTestDialog', @@ -248,10 +342,22 @@ export default { sending: false, notificationForm: { isUrgent: false, - message: '' + message: '', + isPersistent: false }, sentMessages: [], - receiptCleanup: [] + receiptCleanup: [], + persistentNotifications: [], + editDialog: false, + editForm: { + id: null, + message: '', + isUrgent: false, + resend: false + }, + savingEdit: false, + deleteConfirmDialog: false, + itemToDelete: null, } }, computed: { @@ -266,6 +372,7 @@ export default { }, mounted() { this.setupEventListeners() + this.loadPersistentNotifications() }, beforeUnmount() { this.cleanup() @@ -288,10 +395,13 @@ export default { try { // 生成32位随机通知ID const notificationId = this.generateNotificationId() + const messageContent = this.notificationForm.message + const isUrgent = this.notificationForm.isUrgent + const isPersistent = this.notificationForm.isPersistent const result = await this.$refs.eventSender.sendNotification( - this.notificationForm.message, - this.notificationForm.isUrgent, + messageContent, + isUrgent, [], { deviceName: '测试设备', deviceType: 'system', isReadOnly: false }, notificationId @@ -302,8 +412,8 @@ export default { this.sentMessages.push({ id: eventId, notificationId: notificationId, - message: this.notificationForm.message, - isUrgent: this.notificationForm.isUrgent, + message: messageContent, + isUrgent: isUrgent, timestamp: new Date().toISOString(), receipts: { displayed: [], @@ -311,6 +421,36 @@ export default { } }) + // 处理常驻通知 + if (isPersistent) { + try { + const listKey = 'notification-list' + const existingData = await dataProvider.loadData(listKey) + let list = [] + if (existingData && Array.isArray(existingData)) { + list = existingData + } else if (existingData && existingData.success !== false && Array.isArray(existingData.data)) { + // list = existingData.data + list = existingData.data + } + + const newNotification = { + id: notificationId, + message: messageContent, + isUrgent: isUrgent, + timestamp: new Date().toISOString() + } + + list.unshift(newNotification) + await dataProvider.saveData(listKey, list) + // 更新本地列表 + this.persistentNotifications = list + console.log('常驻通知已保存') + } catch (e) { + console.error('保存常驻通知失败', e) + } + } + console.log('通知已发送,事件ID:', eventId, '通知ID:', notificationId) this.resetForm() } catch (error) { @@ -320,6 +460,13 @@ export default { } }, + resetForm() { + this.notificationForm = { + isUrgent: false, + message: '', + isPersistent: false + } + }, close() { this.dialog = false @@ -414,6 +561,137 @@ export default { return receipts.displayed.filter(device => !readSenderIds.includes(device.senderId) ) + }, + + openEditDialog(notification) { + this.editForm = { + id: notification.id, + message: notification.message, + isUrgent: notification.isUrgent || false, + resend: false, + timestamp: notification.timestamp + } + this.editDialog = true + }, + + async saveEdit() { + if (!this.editForm.message.trim()) return + + this.savingEdit = true + try { + // 更新列表 + const index = this.persistentNotifications.findIndex(n => n.id === this.editForm.id) + if (index !== -1) { + this.persistentNotifications[index] = { + ...this.persistentNotifications[index], + message: this.editForm.message, + isUrgent: this.editForm.isUrgent, + // 如果重新发送,更新时间戳?或者保持原样?通常编辑后更新时间戳比较合理 + timestamp: new Date().toISOString() + } + + await dataProvider.saveData('notification-list', this.persistentNotifications) + + // 如果需要重新发送 + if (this.editForm.resend) { + const notificationId = this.editForm.id + const messageContent = this.editForm.message + const isUrgent = this.editForm.isUrgent + + const result = await this.$refs.eventSender.sendNotification( + messageContent, + isUrgent, + [], + { deviceName: '测试设备', deviceType: 'system', isReadOnly: false }, + notificationId + ) + + const eventId = result?.eventId || `msg-${Date.now()}` + + // 添加到发送记录 + this.sentMessages.push({ + id: eventId, + notificationId: notificationId, + message: messageContent, + isUrgent: isUrgent, + timestamp: new Date().toISOString(), + receipts: { + displayed: [], + read: [] + } + }) + } + + this.editDialog = false + this.$message?.success('已更新') + } + } catch (e) { + console.error('保存失败', e) + this.$message?.error('保存失败') + } finally { + this.savingEdit = false + } + }, + + async loadPersistentNotifications() { + try { + const res = await dataProvider.loadData('notification-list') + if (res && Array.isArray(res)) { + this.persistentNotifications = res + } else if (res && res.success !== false && Array.isArray(res.data)) { + this.persistentNotifications = res.data + } else { + this.persistentNotifications = [] + } + } catch (e) { + console.error('加载常驻通知失败', e) + } + }, + + async deleteNotification(notificationId) { + const confirmed = confirm('确定要删除这个通知吗?') + if (!confirmed) return + + try { + // 从 sentMessages 中删除 + this.sentMessages = this.sentMessages.filter(msg => msg.id !== notificationId) + + // 从常驻通知列表中删除 + this.persistentNotifications = this.persistentNotifications.filter(notif => notif.id !== notificationId) + + // TODO: 调用接口删除通知(如果有的话) + + console.log('通知已删除,通知ID:', notificationId) + } catch (error) { + console.error('删除通知失败:', error) + } + }, + + deletePersistentNotification(id) { + this.itemToDelete = id + this.deleteConfirmDialog = true + }, + + async confirmDelete() { + if (!this.itemToDelete) return + + const id = this.itemToDelete + this.deleteConfirmDialog = false + this.itemToDelete = null + + try { + this.persistentNotifications = this.persistentNotifications.filter(n => n.id !== id) + await dataProvider.saveData('notification-list', this.persistentNotifications) + this.$message?.success('已删除') + } catch (e) { + console.error('删除失败', e) + this.$message?.error('删除失败') + } + }, + + confirmDelete(id) { + this.itemToDelete = id + this.deleteConfirmDialog = true } } } diff --git a/src/components/attendance/AttendanceSidebar.vue b/src/components/attendance/AttendanceSidebar.vue index a6596fe..efff8b9 100644 --- a/src/components/attendance/AttendanceSidebar.vue +++ b/src/components/attendance/AttendanceSidebar.vue @@ -1,16 +1,17 @@ @@ -396,6 +488,11 @@ export default { urgentTestDialog: false, // 令牌信息 tokenInfo: null, + + // 常驻通知 + persistentNotifications: [], + notificationDetailDialog: false, + currentNotification: null, }; }, @@ -528,7 +625,17 @@ export default { return getSetting("display.dynamicSort"); }, isEditingDisabled() { - return this.state.uploadLoading || this.state.downloadLoading; + // 检查是否禁用编辑:加载中、没有编辑权限、或被配置禁止编辑过往数据 + if (this.state.uploadLoading || this.state.downloadLoading) return true; + + // 检查是否是只读 token + const manager = this.$refs.studentNameManager; + if (manager?.isReadOnly) return true; + + // 检查是否禁止编辑过往数据 + if (!this.canEditCurrentDate) return true; + + return false; }, unreadCount() { return this.$refs.messageLog?.unreadCount || 0; @@ -542,12 +649,25 @@ export default { confirmNonTodaySave() { return getSetting("edit.confirmNonTodaySave"); }, + blockPastDataEdit() { + return getSetting("edit.blockPastDataEdit"); + }, shouldShowSaveConfirm() { return !this.isToday && this.confirmNonTodaySave; }, shouldBlockAutoSave() { return !this.isToday && this.autoSave && this.blockNonTodayAutoSave; }, + canEditCurrentDate() { + // 检查是否可以编辑当前日期的数据 + if (this.isToday) return true; + if (this.blockPastDataEdit) return false; + return true; + }, + isEditingPastData() { + // 是否正在编辑过往数据(非今日数据) + return !this.isToday; + }, showFullscreenButton() { return getSetting("display.showFullscreenButton"); }, @@ -704,6 +824,9 @@ export default { // 获取令牌信息 await this.loadTokenInfo(); + + // 加载常驻通知 + this.loadPersistentNotifications(); } catch (err) { console.error("初始化失败:", err); this.showError("初始化失败,请刷新页面重试"); @@ -871,6 +994,11 @@ export default { return `${year}${month}${day}`; }, + formatTime(timestamp) { + if (!timestamp) return ''; + return new Date(timestamp).toLocaleString(); + }, + getToday() { return new Date(); }, @@ -1094,6 +1222,19 @@ export default { }, async openDialog(subject) { + // 检查编辑权限 + if (this.isEditingDisabled) { + const manager = this.$refs.studentNameManager; + if (manager?.isReadOnly) { + this.$message.warning("无法编辑", "当前使用的是只读令牌"); + } else if (!this.canEditCurrentDate) { + this.$message.warning("无法编辑", "已禁止编辑过往数据"); + } else { + this.$message.warning("无法编辑", "数据加载中,请稍候"); + } + return; + } + // 如果是自定义卡片 if (subject.startsWith('custom-')) { this.currentEditSubject = subject; @@ -1145,10 +1286,25 @@ export default { }, setAttendanceArea() { + // 检查编辑权限 + if (this.isEditingDisabled) { + this.handleDisabledClick(); + return; + } this.state.attendanceDialog = true; }, - + handleDisabledClick() { + // 处理点击禁用卡片/区域的情况 + const manager = this.$refs.studentNameManager; + if (manager?.isReadOnly) { + this.$message.warning("无法编辑", "当前使用的是只读令牌"); + } else if (!this.canEditCurrentDate) { + this.$message.warning("无法编辑", "已禁止编辑过往数据"); + } else { + this.$message.warning("无法编辑", "数据加载中,请稍候"); + } + }, zoom(direction) { const step = 2; @@ -1182,6 +1338,7 @@ export default { this.state.refreshInterval = setInterval(() => { if (!this.shouldSkipRefresh()) { this.downloadData(); + this.loadPersistentNotifications(); } }, interval * 1000); } @@ -1300,6 +1457,13 @@ export default { const handler = (msg) => { // Expect msg = { uuid, key, action, created?, updatedAt?, deletedAt?, batch? } if (!msg) return; + + // 检查是否是通知列表更新 + if (msg.key === 'notification-list') { + this.loadPersistentNotifications(); + return; + } + // We only care about current date key changes const expectedKey = `classworks-data-${this.state.dateString}`; if (msg.key !== expectedKey) return; @@ -1817,6 +1981,30 @@ export default { console.error("清理URL参数失败:", error); } }, + + async loadPersistentNotifications() { + try { + const res = await dataProvider.loadData('notification-list'); + if (res && Array.isArray(res)) { + this.persistentNotifications = res; + } else if (res && res.success !== false && Array.isArray(res.data)) { + this.persistentNotifications = res.data; + } else { + this.persistentNotifications = []; + } + } catch (e) { + console.error('加载常驻通知失败', e); + } + }, + showNotificationDetail(notification) { + this.currentNotification = notification; + this.notificationDetailDialog = true; + }, + async removePersistentNotification(id) { + this.persistentNotifications = this.persistentNotifications.filter(n => n.id !== id); + await dataProvider.saveData('notification-list', this.persistentNotifications); + this.notificationDetailDialog = false; + }, }, }; diff --git a/src/utils/settings.js b/src/utils/settings.js index 94338f3..def0bcb 100644 --- a/src/utils/settings.js +++ b/src/utils/settings.js @@ -289,6 +289,13 @@ const settingsDefinitions = { description: "保存非当天数据需确认", icon: "mdi-calendar-alert", }, + "edit.blockPastDataEdit": { + type: "boolean", + default: false, + description: "禁止编辑过往数据", + icon: "mdi-lock-clock", + // 启用后将禁止编辑非当天的历史数据,包括作业卡片和出勤统计 + }, "edit.autoSavePromptText": { type: "string", default: "喵?喵呜!",