| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 站内提醒横幅:即将开始的会议 --> |
| | | <el-alert |
| | | v-if="upcomingMeetings.length" |
| | | class="remind-banner" |
| | | type="warning" |
| | | :closable="false" |
| | | show-icon |
| | | > |
| | | <template #title> |
| | | 会议提醒:{{ upcomingText }} |
| | | </template> |
| | | </el-alert> |
| | | |
| | | <div class="tabs-wrapper"> |
| | | <el-tabs |
| | | v-model="activeTab" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed } from 'vue' |
| | | import { ref, computed, onMounted, onUnmounted } from 'vue' |
| | | import { ElNotification } from 'element-plus' |
| | | import dayjs from 'dayjs' |
| | | import { getMeetingPublish } from '@/api/collaborativeApproval/meeting.js' |
| | | import MeetSetting from '../notificationManagement/meetSetting/index.vue' |
| | | import MeetIndex from '../notificationManagement/meetIndex/index.vue' |
| | | import MeetApplication from '../notificationManagement/meetApplication/index.vue' |
| | |
| | | function handleTabChange(name) { |
| | | activeTab.value = name |
| | | } |
| | | |
| | | /* ==================== 会议提醒(站内 + 浏览器/手机端弹窗) ==================== */ |
| | | const REMIND_WINDOW_MIN = 30 // 会议开始前 30 分钟内提醒 |
| | | const REMIND_STORAGE_KEY = 'meeting_remind_sent' |
| | | const POLL_INTERVAL = 60 * 1000 // 每 60 秒检查一次 |
| | | |
| | | const upcomingMeetings = ref([]) |
| | | let remindTimer = null |
| | | |
| | | const getSentMap = () => { |
| | | try { |
| | | return JSON.parse(localStorage.getItem(REMIND_STORAGE_KEY) || '{}') |
| | | } catch (e) { |
| | | return {} |
| | | } |
| | | } |
| | | const markSent = (key) => { |
| | | const m = getSentMap() |
| | | m[key] = Date.now() |
| | | // 只保留最近 200 条,避免 localStorage 无限增长 |
| | | const entries = Object.entries(m).sort((a, b) => b[1] - a[1]).slice(0, 200) |
| | | localStorage.setItem(REMIND_STORAGE_KEY, JSON.stringify(Object.fromEntries(entries))) |
| | | } |
| | | |
| | | // 计算会议开始时间(兼容 startTime 为 datetime / 纯时间两种格式) |
| | | const parseStart = (m) => { |
| | | const raw = m.startTime |
| | | if (!raw) return null |
| | | const s = String(raw) |
| | | if (s.length > 10) return dayjs(s) |
| | | if (!m.meetingDate) return null |
| | | return dayjs(`${m.meetingDate} ${s}`) |
| | | } |
| | | |
| | | const checkRemind = async () => { |
| | | try { |
| | | const resp = await getMeetingPublish({ current: 1, size: 100 }) |
| | | const records = (resp && resp.data && resp.data.records) || [] |
| | | const now = dayjs() |
| | | const sent = getSentMap() |
| | | const upcoming = [] |
| | | |
| | | records.forEach((m) => { |
| | | const start = parseStart(m) |
| | | if (!start || !start.isValid()) return |
| | | const diffMin = start.diff(now, 'minute', true) |
| | | // 只提醒「即将开始」:0 ~ 30 分钟 |
| | | if (diffMin < 0 || diffMin > REMIND_WINDOW_MIN) return |
| | | const flagKey = `${m.id}_${start.format('YYYYMMDDHHmm')}` |
| | | upcoming.push({ title: m.title || '会议', start, diffMin }) |
| | | |
| | | if (sent[flagKey]) return |
| | | markSent(flagKey) |
| | | ElNotification({ |
| | | title: '会议提醒', |
| | | message: `【${m.title || '会议'}】将于 ${start.format('HH:mm')} 开始${ |
| | | diffMin <= 1 ? '(即将开始)' : `(约 ${Math.round(diffMin)} 分钟后)` |
| | | }`, |
| | | type: 'warning', |
| | | duration: 15000, |
| | | position: 'bottom-right' |
| | | }) |
| | | }) |
| | | |
| | | upcoming.sort((a, b) => a.start.valueOf() - b.start.valueOf()) |
| | | upcomingMeetings.value = upcoming |
| | | } catch (e) { |
| | | // 提醒失败不影响主流程 |
| | | } |
| | | } |
| | | |
| | | const upcomingText = computed(() => |
| | | upcomingMeetings.value |
| | | .map((x) => `${x.title}(${x.start.format('MM-DD HH:mm')})`) |
| | | .join(';') |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | checkRemind() |
| | | remindTimer = setInterval(checkRemind, POLL_INTERVAL) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | if (remindTimer) { |
| | | clearInterval(remindTimer) |
| | | remindTimer = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .remind-banner { |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .tabs-wrapper { |
| | | margin-bottom: 10px; |
| | |
| | | min-height: 400px; |
| | | } |
| | | </style> |
| | | |