1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-08 13:49:37 +00:00

feat: 添加常驻通知管理功能,支持编辑和删除通知

This commit is contained in:
Sunwuyuan 2025-11-30 11:50:20 +08:00
parent 069f0a31c0
commit 46dffb02ca
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
6 changed files with 564 additions and 26 deletions

View File

@ -213,6 +213,26 @@
</div> </div>
</v-card-text> </v-card-text>
<!-- 非今日编辑警告 -->
<v-alert
v-if="isEditingPastData"
type="warning"
variant="tonal"
class="mx-4 mb-4"
border="start"
border-color="warning"
prominent
>
<template #prepend>
</template>
<div class="d-flex flex-column">
<div class="text-h6 mb-1">你打算修改历史</div>
<div class="text-body-2">
这是 {{ new Date(currentDateString.slice(0,4), currentDateString.slice(4,6)-1, currentDateString.slice(6,8)).toLocaleDateString() }} 的作业 请谨慎操作确保不会覆盖重要数据
</div>
</div>
</v-alert>
<div class="text-center text-body-2 text-disabled mb-5"> <div class="text-center text-body-2 text-disabled mb-5">
点击空白处完成编辑 点击空白处完成编辑
</div> </div>
@ -244,6 +264,14 @@ export default {
autoSave: { autoSave: {
type: Boolean, type: Boolean,
default: false default: false
},
isEditingPastData: {
type: Boolean,
default: false
},
currentDateString: {
type: String,
default: ""
} }
}, },
emits: ["update:modelValue", "save"], emits: ["update:modelValue", "save"],

View File

@ -43,8 +43,14 @@
color="red" color="red"
inset inset
> >
</v-switch> </v-switch>
<v-checkbox
v-model="notificationForm.isPersistent"
label="常驻展示"
color="primary"
hide-details
class="mt-0"
></v-checkbox>
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea
@ -82,6 +88,42 @@
</v-col> </v-col>
</v-row> </v-row>
<!-- 常驻通知管理 -->
<v-row class="mt-4">
<v-col cols="12">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-pin</v-icon>
常驻通知管理
</v-card-title>
<v-card-text>
<div v-if="persistentNotifications.length === 0" class="text-center text-grey py-4">
暂无常驻通知
</div>
<v-list v-else>
<v-list-item
v-for="item in persistentNotifications"
:key="item.id"
:title="item.message"
:subtitle="formatTime(item.timestamp)"
lines="two"
>
<template v-slot:prepend>
<v-icon :color="item.isUrgent ? 'error' : 'primary'">
{{ item.isUrgent ? 'mdi-alert-circle' : 'mdi-information' }}
</v-icon>
</template>
<template v-slot:append>
<v-btn icon="mdi-pencil" variant="text" size="small" @click="openEditDialog(item)"></v-btn>
<v-btn icon="mdi-delete" variant="text" color="error" size="small" @click="confirmDelete(item.id)"></v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 消息发送历史 --> <!-- 消息发送历史 -->
<v-row class="mt-4"> <v-row class="mt-4">
<v-col cols="12"> <v-col cols="12">
@ -222,6 +264,57 @@
<ChatWidget /> <ChatWidget />
<EventSender ref="eventSender" /> <EventSender ref="eventSender" />
<!-- 编辑常驻通知对话框 -->
<v-dialog v-model="editDialog" max-width="500" :fullscreen="$vuetify.display.xs">
<v-card>
<v-toolbar flat density="compact">
<v-toolbar-title>编辑常驻通知</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon="mdi-close" @click="editDialog = false"></v-btn>
</v-toolbar>
<v-card-text>
<v-form>
<v-textarea
v-model="editForm.message"
label="通知内容"
rows="3"
auto-grow
></v-textarea>
<v-switch
v-model="editForm.isUrgent"
label="强调通知"
color="error"
hide-details
></v-switch>
<v-checkbox
v-model="editForm.resend"
label="保存并重新发送通知"
hint="勾选后将作为新通知发送给所有在线设备"
persistent-hint
></v-checkbox>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="editDialog = false">取消</v-btn>
<v-btn color="primary" :loading="savingEdit" @click="saveEdit">保存</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteConfirmDialog" max-width="400">
<v-card>
<v-card-title class="text-h5">确认删除</v-card-title>
<v-card-text>确定要删除这条常驻通知吗此操作无法撤销</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="deleteConfirmDialog = false">取消</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete">删除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-dialog> </v-dialog>
</template> </template>
@ -229,6 +322,7 @@
import ChatWidget from '@/components/ChatWidget.vue' import ChatWidget from '@/components/ChatWidget.vue'
import EventSender from '@/components/EventSender.vue' import EventSender from '@/components/EventSender.vue'
import { on as socketOn } from '@/utils/socketClient' import { on as socketOn } from '@/utils/socketClient'
import dataProvider from '@/utils/dataProvider'
export default { export default {
name: 'UrgentTestDialog', name: 'UrgentTestDialog',
@ -248,10 +342,22 @@ export default {
sending: false, sending: false,
notificationForm: { notificationForm: {
isUrgent: false, isUrgent: false,
message: '' message: '',
isPersistent: false
}, },
sentMessages: [], sentMessages: [],
receiptCleanup: [] receiptCleanup: [],
persistentNotifications: [],
editDialog: false,
editForm: {
id: null,
message: '',
isUrgent: false,
resend: false
},
savingEdit: false,
deleteConfirmDialog: false,
itemToDelete: null,
} }
}, },
computed: { computed: {
@ -266,6 +372,7 @@ export default {
}, },
mounted() { mounted() {
this.setupEventListeners() this.setupEventListeners()
this.loadPersistentNotifications()
}, },
beforeUnmount() { beforeUnmount() {
this.cleanup() this.cleanup()
@ -288,10 +395,13 @@ export default {
try { try {
// 32ID // 32ID
const notificationId = this.generateNotificationId() 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( const result = await this.$refs.eventSender.sendNotification(
this.notificationForm.message, messageContent,
this.notificationForm.isUrgent, isUrgent,
[], [],
{ deviceName: '测试设备', deviceType: 'system', isReadOnly: false }, { deviceName: '测试设备', deviceType: 'system', isReadOnly: false },
notificationId notificationId
@ -302,8 +412,8 @@ export default {
this.sentMessages.push({ this.sentMessages.push({
id: eventId, id: eventId,
notificationId: notificationId, notificationId: notificationId,
message: this.notificationForm.message, message: messageContent,
isUrgent: this.notificationForm.isUrgent, isUrgent: isUrgent,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
receipts: { receipts: {
displayed: [], 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) console.log('通知已发送事件ID:', eventId, '通知ID:', notificationId)
this.resetForm() this.resetForm()
} catch (error) { } catch (error) {
@ -320,6 +460,13 @@ export default {
} }
}, },
resetForm() {
this.notificationForm = {
isUrgent: false,
message: '',
isPersistent: false
}
},
close() { close() {
this.dialog = false this.dialog = false
@ -414,6 +561,137 @@ export default {
return receipts.displayed.filter(device => return receipts.displayed.filter(device =>
!readSenderIds.includes(device.senderId) !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
} }
} }
} }

View File

@ -1,16 +1,17 @@
<template> <template>
<v-col <v-col
v-if="studentList && studentList.length" v-if="studentList && studentList.length"
v-ripple="{ v-ripple="!isEditingDisabled ? {
class: `text-${ class: `text-${
['primary', 'secondary', 'info', 'success', 'warning', 'error'][ ['primary', 'secondary', 'info', 'success', 'warning', 'error'][
Math.floor(Math.random() * 6) Math.floor(Math.random() * 6)
] ]
}`, }`,
}" } : false"
:class="{ 'cursor-not-allowed': isEditingDisabled }"
class="attendance-area no-select" class="attendance-area no-select"
cols="1" cols="1"
@click="$emit('click')" @click="handleClick"
> >
<h1>出勤</h1> <h1>出勤</h1>
<h2> <h2>
@ -94,12 +95,25 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isEditingDisabled: {
type: Boolean,
default: false,
}, },
emits: ["click"], },
emits: ["click", "disabled-click"],
setup() { setup() {
const display = useDisplay(); const display = useDisplay();
return { display }; return { display };
}, },
methods: {
handleClick() {
if (this.isEditingDisabled) {
this.$emit('disabled-click');
} else {
this.$emit('click');
}
},
},
}; };
</script> </script>

View File

@ -13,11 +13,11 @@
<!-- 出勤卡片 --> <!-- 出勤卡片 -->
<v-card <v-card
v-if="item.type === 'attendance'" v-if="item.type === 'attendance'"
:class="{ 'glow-highlight': highlightedCards[item.key] }" :class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
border border
class="glow-track" class="glow-track"
height="100%" height="100%"
@click="$emit('open-attendance')" @click="handleCardClick('attendance', null)"
@mousemove="handleMouseMove" @mousemove="handleMouseMove"
@touchmove="handleTouchMove" @touchmove="handleTouchMove"
> >
@ -82,11 +82,11 @@
<!-- 自定义/测试卡片 --> <!-- 自定义/测试卡片 -->
<v-card <v-card
v-else-if="item.type === 'custom'" v-else-if="item.type === 'custom'"
:class="{ 'glow-highlight': highlightedCards[item.key] }" :class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
border border
class="glow-track" class="glow-track"
height="100%" height="100%"
@click="!isEditingDisabled && $emit('open-dialog', item.key)" @click="handleCardClick('dialog', item.key)"
@mousemove="handleMouseMove" @mousemove="handleMouseMove"
@touchmove="handleTouchMove" @touchmove="handleTouchMove"
> >
@ -102,11 +102,11 @@
<!-- 普通作业卡片 --> <!-- 普通作业卡片 -->
<v-card <v-card
v-else v-else
:class="{ 'glow-highlight': highlightedCards[item.key] }" :class="{ 'glow-highlight': highlightedCards[item.key], 'cursor-not-allowed': isEditingDisabled, 'cursor-pointer': !isEditingDisabled }"
border border
class="glow-track" class="glow-track"
height="100%" height="100%"
@click="!isEditingDisabled && $emit('open-dialog', item.key)" @click="handleCardClick('dialog', item.key)"
@mousemove="handleMouseMove" @mousemove="handleMouseMove"
@touchmove="handleTouchMove" @touchmove="handleTouchMove"
> >
@ -133,11 +133,10 @@
<v-chip <v-chip
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.name" :key="subject.name"
:disabled="isEditingDisabled"
class="ma-1" class="ma-1"
color="primary" color="primary"
variant="tonal" variant="tonal"
@click="$emit('open-dialog', subject.name)" @click="handleCardClick('dialog', subject.name)"
> >
<v-icon start size="small">mdi-plus</v-icon> <v-icon start size="small">mdi-plus</v-icon>
{{ subject.name }} {{ subject.name }}
@ -149,8 +148,7 @@
<v-btn <v-btn
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.name" :key="subject.name"
:disabled="isEditingDisabled" @click="handleCardClick('dialog', subject.name)"
@click="$emit('open-dialog', subject.name)"
> >
<v-icon start> mdi-plus</v-icon> <v-icon start> mdi-plus</v-icon>
{{ subject.name }} {{ subject.name }}
@ -162,10 +160,9 @@
<v-card <v-card
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.name" :key="subject.name"
:disabled="isEditingDisabled"
border border
class="empty-subject-card" class="empty-subject-card"
@click="$emit('open-dialog', subject.name)" @click="handleCardClick('dialog', subject.name)"
> >
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
{{ subject.name }} {{ subject.name }}
@ -209,13 +206,25 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
emits: ["open-dialog", "open-attendance"], emits: ["open-dialog", "open-attendance", "disabled-click"],
computed: { computed: {
isMobile() { isMobile() {
return this.$vuetify.display.mobile; return this.$vuetify.display.mobile;
}, },
}, },
methods: { 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) { splitPoint(content) {
return content.split("\n").filter((text) => text.trim()); return content.split("\n").filter((text) => text.trim());
}, },
@ -241,3 +250,17 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
.cursor-not-allowed {
cursor: not-allowed !important;
}
.cursor-pointer {
cursor: pointer;
}
.v-card.cursor-not-allowed:hover {
transform: none !important;
}
</style>

View File

@ -66,6 +66,77 @@
<div v-if="!shouldShowInit" class="d-flex"> <div v-if="!shouldShowInit" class="d-flex">
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<v-container class="main-window flex-grow-1 no-select bloom-container" fluid> <v-container class="main-window flex-grow-1 no-select bloom-container" fluid>
<!-- 常驻通知区域 -->
<v-row v-if="persistentNotifications.length > 0" class="mb-4">
<v-col cols="12">
<v-card
v-for="notification in persistentNotifications"
:key="notification.id"
:color="notification.isUrgent ? 'error' : 'primary'"
class="mb-2 cursor-pointer"
variant="tonal"
@click="showNotificationDetail(notification)"
>
<v-card-text class="d-flex align-center py-3">
<span class="text-h6 text-truncate font-weight-bold">{{ notification.message }}</span>
<v-spacer></v-spacer>
<v-btn icon="mdi-chevron-right" variant="text"></v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 通知详情对话框 -->
<v-dialog v-model="notificationDetailDialog" max-width="700" scrollable>
<v-card v-if="currentNotification" class="rounded-xl">
<v-card-title class="d-flex align-center pa-4 text-h5">
<span :class="currentNotification.isUrgent ? 'text-error' : ''" class="font-weight-bold">
{{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
</span>
<v-spacer></v-spacer>
<v-btn icon="mdi-close" variant="text" @click="notificationDetailDialog = false"></v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-6">
<div class="text-h4 font-weight-medium mb-4" style="line-height: 1.5;">
{{ currentNotification.message }}
</div>
<div class="text-subtitle-1 text-grey">
发布时间{{ formatTime(currentNotification.timestamp) }}
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-btn
color="error"
prepend-icon="mdi-delete"
size="x-large"
variant="tonal"
class="px-6"
@click="removePersistentNotification(currentNotification.id)"
>
删除通知
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="primary"
size="x-large"
variant="elevated"
class="px-8"
@click="notificationDetailDialog = false"
>
关闭
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<homework-grid <homework-grid
:sorted-items="sortedItems" :sorted-items="sortedItems"
:unused-subjects="unusedSubjects" :unused-subjects="unusedSubjects"
@ -75,6 +146,7 @@
:highlighted-cards="highlightedCards" :highlighted-cards="highlightedCards"
@open-dialog="openDialog" @open-dialog="openDialog"
@open-attendance="setAttendanceArea" @open-attendance="setAttendanceArea"
@disabled-click="handleDisabledClick"
/> />
<home-actions <home-actions
@ -100,7 +172,9 @@
v-if="!mobile" v-if="!mobile"
:student-list="state.studentList" :student-list="state.studentList"
:attendance="state.boardData.attendance" :attendance="state.boardData.attendance"
:is-editing-disabled="isEditingDisabled"
@click="setAttendanceArea" @click="setAttendanceArea"
@disabled-click="handleDisabledClick"
/> />
</div> </div>
@ -109,6 +183,8 @@
:auto-save="autoSave" :auto-save="autoSave"
:initial-content="state.textarea" :initial-content="state.textarea"
:title="state.dialogTitle" :title="state.dialogTitle"
:is-editing-past-data="isEditingPastData"
:current-date-string="state.dateString"
@save="handleHomeworkSave" @save="handleHomeworkSave"
/> />
@ -220,6 +296,22 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- 通知详情对话框 -->
<v-dialog v-model="notificationDetailDialog" max-width="600">
<v-card v-if="currentNotification">
<v-card-title class="headline" :class="currentNotification.isUrgent ? 'text-error' : 'text-primary'">
{{ currentNotification.isUrgent ? '强调通知' : '通知详情' }}
</v-card-title>
<v-card-text class="text-h5 py-4">
{{ currentNotification.message }}
</v-card-text>
<v-card-actions>
<v-btn color="error" variant="text" @click="removePersistentNotification(currentNotification.id)">删除</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" @click="notificationDetailDialog = false">关闭</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<br /><br /><br /> <br /><br /><br />
</template> </template>
@ -396,6 +488,11 @@ export default {
urgentTestDialog: false, urgentTestDialog: false,
// //
tokenInfo: null, tokenInfo: null,
//
persistentNotifications: [],
notificationDetailDialog: false,
currentNotification: null,
}; };
}, },
@ -528,7 +625,17 @@ export default {
return getSetting("display.dynamicSort"); return getSetting("display.dynamicSort");
}, },
isEditingDisabled() { 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() { unreadCount() {
return this.$refs.messageLog?.unreadCount || 0; return this.$refs.messageLog?.unreadCount || 0;
@ -542,12 +649,25 @@ export default {
confirmNonTodaySave() { confirmNonTodaySave() {
return getSetting("edit.confirmNonTodaySave"); return getSetting("edit.confirmNonTodaySave");
}, },
blockPastDataEdit() {
return getSetting("edit.blockPastDataEdit");
},
shouldShowSaveConfirm() { shouldShowSaveConfirm() {
return !this.isToday && this.confirmNonTodaySave; return !this.isToday && this.confirmNonTodaySave;
}, },
shouldBlockAutoSave() { shouldBlockAutoSave() {
return !this.isToday && this.autoSave && this.blockNonTodayAutoSave; 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() { showFullscreenButton() {
return getSetting("display.showFullscreenButton"); return getSetting("display.showFullscreenButton");
}, },
@ -704,6 +824,9 @@ export default {
// //
await this.loadTokenInfo(); await this.loadTokenInfo();
//
this.loadPersistentNotifications();
} catch (err) { } catch (err) {
console.error("初始化失败:", err); console.error("初始化失败:", err);
this.showError("初始化失败,请刷新页面重试"); this.showError("初始化失败,请刷新页面重试");
@ -871,6 +994,11 @@ export default {
return `${year}${month}${day}`; return `${year}${month}${day}`;
}, },
formatTime(timestamp) {
if (!timestamp) return '';
return new Date(timestamp).toLocaleString();
},
getToday() { getToday() {
return new Date(); return new Date();
}, },
@ -1094,6 +1222,19 @@ export default {
}, },
async openDialog(subject) { 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-')) { if (subject.startsWith('custom-')) {
this.currentEditSubject = subject; this.currentEditSubject = subject;
@ -1145,10 +1286,25 @@ export default {
}, },
setAttendanceArea() { setAttendanceArea() {
//
if (this.isEditingDisabled) {
this.handleDisabledClick();
return;
}
this.state.attendanceDialog = true; 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) { zoom(direction) {
const step = 2; const step = 2;
@ -1182,6 +1338,7 @@ export default {
this.state.refreshInterval = setInterval(() => { this.state.refreshInterval = setInterval(() => {
if (!this.shouldSkipRefresh()) { if (!this.shouldSkipRefresh()) {
this.downloadData(); this.downloadData();
this.loadPersistentNotifications();
} }
}, interval * 1000); }, interval * 1000);
} }
@ -1300,6 +1457,13 @@ export default {
const handler = (msg) => { const handler = (msg) => {
// Expect msg = { uuid, key, action, created?, updatedAt?, deletedAt?, batch? } // Expect msg = { uuid, key, action, created?, updatedAt?, deletedAt?, batch? }
if (!msg) return; if (!msg) return;
//
if (msg.key === 'notification-list') {
this.loadPersistentNotifications();
return;
}
// We only care about current date key changes // We only care about current date key changes
const expectedKey = `classworks-data-${this.state.dateString}`; const expectedKey = `classworks-data-${this.state.dateString}`;
if (msg.key !== expectedKey) return; if (msg.key !== expectedKey) return;
@ -1817,6 +1981,30 @@ export default {
console.error("清理URL参数失败:", error); 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;
},
}, },
}; };
</script> </script>

View File

@ -289,6 +289,13 @@ const settingsDefinitions = {
description: "保存非当天数据需确认", description: "保存非当天数据需确认",
icon: "mdi-calendar-alert", icon: "mdi-calendar-alert",
}, },
"edit.blockPastDataEdit": {
type: "boolean",
default: false,
description: "禁止编辑过往数据",
icon: "mdi-lock-clock",
// 启用后将禁止编辑非当天的历史数据,包括作业卡片和出勤统计
},
"edit.autoSavePromptText": { "edit.autoSavePromptText": {
type: "string", type: "string",
default: "喵?喵呜!", default: "喵?喵呜!",