成绩通知:仅在某门课程成绩正式发布后自动发送,按成绩单学生名单通知,不展示具体分数;发布状态与通知同一次提交。
手动发信:移除“成绩通知”选项,只能发送普通通知,原有角色与范围权限保持不变。 选课通知:管理员关闭一轮选课时,自动向每位参与学生发送一条汇总通知,包含最终选中课程及候补未成功课程;候补失效、轮次关闭和通知生成保持原子性。 关闭选课界面会明确提示发送通知,并显示实际通知人数。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Bell, Check, Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import { Check, Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
|
||||
@@ -12,8 +12,6 @@ const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isMana
|
||||
|
||||
const adjustments = ref<any[]>([])
|
||||
const pendingReviews = ref<any[]>([])
|
||||
const notifications = ref<any[]>([])
|
||||
const unreadCount = ref(0)
|
||||
const loading = ref(false)
|
||||
const reviewLoading = ref(false)
|
||||
const dialog = ref(false)
|
||||
@@ -61,19 +59,10 @@ async function load() {
|
||||
params: { academicTermId: termId.value },
|
||||
})).data
|
||||
}
|
||||
await loadNotifications()
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const { data } = await http.get('/notifications', { params: { pageSize: 50 } })
|
||||
notifications.value = data.items
|
||||
unreadCount.value = data.unreadCount
|
||||
} catch (_) { /* notifications are optional */ }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, {
|
||||
teachingTaskId: undefined,
|
||||
@@ -146,23 +135,6 @@ async function doReject() {
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
|
||||
async function markRead(n: any) {
|
||||
if (n.isRead) return
|
||||
try {
|
||||
await http.post(`/notifications/${n.id}/read`)
|
||||
n.isRead = true
|
||||
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function markAllRead() {
|
||||
try {
|
||||
await http.post('/notifications/read-all')
|
||||
notifications.value.forEach(n => n.isRead = true)
|
||||
unreadCount.value = 0
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [termRes, taskRes] = await Promise.all([
|
||||
@@ -195,9 +167,6 @@ function showCancel(type: string) { return type === 'Cancel' }
|
||||
<p>教师提交调课、停课、补课、代课申请,学院审核后生效并通知相关人员。</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<el-badge :value="unreadCount" :hidden="!unreadCount">
|
||||
<el-button :icon="Bell" @click="tab = 'notifications'">消息</el-button>
|
||||
</el-badge>
|
||||
<el-button v-if="isTeacher" type="primary" :icon="Plus" @click="openCreate">提交申请</el-button>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</div>
|
||||
@@ -210,7 +179,6 @@ function showCancel(type: string) { return type === 'Cancel' }
|
||||
<el-segmented v-model="tab" :options="[
|
||||
...(isTeacher ? [{ label: '我的申请', value: 'mine' }] : []),
|
||||
...(isManager ? [{ label: '待审核', value: 'reviews' }] : []),
|
||||
{ label: '消息通知', value: 'notifications' },
|
||||
]" />
|
||||
</section>
|
||||
|
||||
@@ -284,22 +252,6 @@ function showCancel(type: string) { return type === 'Cancel' }
|
||||
<el-empty v-if="!pendingReviews.length" description="暂无待审核申请" />
|
||||
</section>
|
||||
|
||||
<!-- Notifications -->
|
||||
<section v-if="tab === 'notifications'" class="adj-list">
|
||||
<div v-if="unreadCount" class="notif-actions">
|
||||
<el-button size="small" text @click="markAllRead">全部标为已读</el-button>
|
||||
</div>
|
||||
<article v-for="n in notifications" :key="n.id" class="notif-card" :class="{ unread: !n.isRead }" @click="markRead(n)">
|
||||
<div class="notif-dot" v-if="!n.isRead" />
|
||||
<div>
|
||||
<b>{{ n.title }}</b>
|
||||
<p>{{ n.content }}</p>
|
||||
<small>{{ new Date(n.createdAt).toLocaleString('zh-CN') }}</small>
|
||||
</div>
|
||||
</article>
|
||||
<el-empty v-if="!notifications.length" description="暂无消息通知" />
|
||||
</section>
|
||||
|
||||
<!-- Create dialog -->
|
||||
<el-dialog v-model="dialog" title="提交调停课申请" width="680px" top="5vh">
|
||||
<el-form label-position="top">
|
||||
@@ -410,11 +362,4 @@ function showCancel(type: string) { return type === 'Cancel' }
|
||||
.adj-card.approved { border-left: 4px solid #67c23a; }
|
||||
.adj-card.rejected { border-left: 4px solid #f56c6c; }
|
||||
|
||||
.notif-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 18px; background: #fff; border: 1px solid #e4e7ed; border-radius: 8px; cursor: pointer; }
|
||||
.notif-card.unread { background: #ecf5ff; border-color: #b3d8ff; }
|
||||
.notif-dot { width: 8px; height: 8px; border-radius: 50%; background: #409eff; flex-shrink: 0; margin-top: 6px; }
|
||||
.notif-card b { font-size: 14px; display: block; margin-bottom: 4px; }
|
||||
.notif-card p { font-size: 13px; color: #606266; margin: 0; }
|
||||
.notif-card small { font-size: 11px; color: var(--muted); }
|
||||
.notif-actions { display: flex; justify-content: flex-end; margin-bottom: 8px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user