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
+ 常驻通知管理
+
+
+
+ 暂无常驻通知
+
+
+
+
+
+ {{ item.isUrgent ? 'mdi-alert-circle' : 'mdi-information' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 @@
出勤
@@ -94,12 +95,25 @@ export default {
type: Object,
required: true,
},
+ isEditingDisabled: {
+ type: Boolean,
+ default: false,
+ },
},
- emits: ["click"],
+ emits: ["click", "disabled-click"],
setup() {
const display = useDisplay();
return { display };
},
+ methods: {
+ handleClick() {
+ if (this.isEditingDisabled) {
+ this.$emit('disabled-click');
+ } else {
+ this.$emit('click');
+ }
+ },
+ },
};
diff --git a/src/components/home/HomeworkGrid.vue b/src/components/home/HomeworkGrid.vue
index 0f921d1..b5182ea 100644
--- a/src/components/home/HomeworkGrid.vue
+++ b/src/components/home/HomeworkGrid.vue
@@ -13,11 +13,11 @@
@@ -82,11 +82,11 @@
@@ -102,11 +102,11 @@
@@ -133,11 +133,10 @@
mdi-plus
{{ subject.name }}
@@ -149,8 +148,7 @@
mdi-plus
{{ subject.name }}
@@ -162,10 +160,9 @@
{{ subject.name }}
@@ -209,13 +206,25 @@ export default {
default: () => ({}),
},
},
- emits: ["open-dialog", "open-attendance"],
+ emits: ["open-dialog", "open-attendance", "disabled-click"],
computed: {
isMobile() {
return this.$vuetify.display.mobile;
},
},
methods: {
+ handleCardClick(type, key) {
+ if (this.isEditingDisabled) {
+ this.$emit('disabled-click');
+ return;
+ }
+
+ if (type === 'attendance') {
+ this.$emit('open-attendance');
+ } else if (type === 'dialog') {
+ this.$emit('open-dialog', key);
+ }
+ },
splitPoint(content) {
return content.split("\n").filter((text) => text.trim());
},
@@ -241,3 +250,17 @@ export default {
},
};
+
+
diff --git a/src/pages/index.vue b/src/pages/index.vue
index 2a96451..7b4166a 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -66,6 +66,77 @@
+
+
+
+
+
+
+ {{ notification.message }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
+
+
+
+
+
+
+
+
+
+ {{ currentNotification.message }}
+
+
+ 发布时间:{{ formatTime(currentNotification.timestamp) }}
+
+
+
+
+
+
+
+ 删除通知
+
+
+
+ 关闭
+
+
+
+
+
@@ -109,6 +183,8 @@
:auto-save="autoSave"
:initial-content="state.textarea"
:title="state.dialogTitle"
+ :is-editing-past-data="isEditingPastData"
+ :current-date-string="state.dateString"
@save="handleHomeworkSave"
/>
@@ -220,6 +296,22 @@
+
+
+
+
+ {{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
+
+
+ {{ currentNotification.message }}
+
+
+ 删除
+
+ 关闭
+
+
+
@@ -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: "喵?喵呜!",