src/views/collaborativeApproval/meetingManagement/index.vue
@@ -1,5 +1,18 @@
<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"
@@ -24,7 +37,10 @@
</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'
@@ -48,9 +64,100 @@
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;
@@ -60,4 +167,3 @@
  min-height: 400px;
}
</style>