CompanyCalendarDialog.vue 14.9 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="90%"
    :close-on-click-modal="false"
    custom-class="company-calendar-dialog"
    append-to-body
  >
    <div class="dialog-inner">
      <div class="dialog-header">
        <div class="dialog-title">公司日历</div>
        <div class="dialog-legend">
          <span v-for="item in legendList" :key="item.type" class="legend-item">
            <span class="legend-dot" :style="{ background: item.color }"></span>
            {{ item.label }}
          </span>
        </div>
        <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div class="dialog-content" v-loading="loading">
        <FullCalendar
          ref="fullCalendar"
          class="company-calendar"
          defaultView="dayGridMonth"
          :header="calendarHeader"
          :plugins="calendarPlugins"
          :weekends="true"
          :events="calendarEvents"
          locale="zh-cn"
          :buttonText="buttonText"
          :height="calendarHeight"
          :eventLimit="true"
          allDayText="全天"
          :editable="false"
          @datesRender="datesRender"
          @eventClick="handleEventClick"
        />
      </div>
    </div>
  </el-dialog>
</template>

<script>
import FullCalendar from '@fullcalendar/vue'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'

export default {
  name: 'CompanyCalendarDialog',
  components: { FullCalendar },
  props: {
    visible: { type: Boolean, default: false }
  },
  data() {
    const today = new Date()
    const y = today.getFullYear()
    const m = String(today.getMonth() + 1).padStart(2, '0')
    const d = String(today.getDate()).padStart(2, '0')
    const todayStr = `${y}-${m}-${d}`
    return {
      loading: false,
      // 公司层面的会议/活动数据(后续可换成接口)
      companyEvents: [
        // 当天 7 条示例(操作会 + 大会 + 活动),方便预览密集情况
        { id: 'E001', date: todayStr, start: `${todayStr}T09:00:00`, end: `${todayStr}T10:00:00`, type: 'ops', title: '操作会 - 紫荆店' },
        { id: 'E002', date: todayStr, start: `${todayStr}T10:30:00`, end: `${todayStr}T11:30:00`, type: 'ops', title: '操作会 - 西沙店' },
        { id: 'E003', date: todayStr, start: `${todayStr}T13:00:00`, end: `${todayStr}T14:00:00`, type: 'training', title: '培训会 - 新员工入职营' },
        { id: 'E004', date: todayStr, start: `${todayStr}T14:30:00`, end: `${todayStr}T15:30:00`, type: 'training', title: '培训会 - 科美新品讲解' },
        { id: 'E005', date: todayStr, start: `${todayStr}T16:00:00`, end: `${todayStr}T17:00:00`, type: 'activity', title: '门店活动 - 紫荆店会员沙龙' },
        { id: 'E006', date: todayStr, start: `${todayStr}T18:00:00`, end: `${todayStr}T19:00:00`, type: 'activity', title: '门店活动 - 西沙店体验日' },
        { id: 'E007', date: todayStr, start: `${todayStr}T19:30:00`, end: `${todayStr}T21:00:00`, type: 'meeting', title: '全体大会 - 本月复盘' },
        // 其它日期:尽量覆盖当月大部分天
        { id: 'E008',  date: `${y}-${m}-01`, start: `${y}-${m}-01T10:00:00`, end: `${y}-${m}-01T12:00:00`, type: 'meeting',  title: '月初经营例会' },
        { id: 'E009',  date: `${y}-${m}-02`, start: `${y}-${m}-02T14:00:00`, end: `${y}-${m}-02T16:00:00`, type: 'ops',     title: '操作会 - 静居寺店' },
        { id: 'E010', date: `${y}-${m}-03`, start: `${y}-${m}-03T09:30:00`, end: `${y}-${m}-03T11:00:00`, type: 'training', title: '培训会 - 会员沟通话术' },
        { id: 'E011', date: `${y}-${m}-04`, start: `${y}-${m}-04T15:00:00`, end: `${y}-${m}-04T17:00:00`, type: 'activity', title: '门店活动 - 双流店开放日' },
        { id: 'E012', date: `${y}-${m}-05`, start: `${y}-${m}-05T10:00:00`, end: `${y}-${m}-05T11:30:00`, type: 'ops',     title: '操作会 - 保利店' },
        { id: 'E013', date: `${y}-${m}-06`, start: `${y}-${m}-06T14:00:00`, end: `${y}-${m}-06T15:30:00`, type: 'training', title: '培训会 - 逆龄项目进阶' },
        { id: 'E014', date: `${y}-${m}-07`, start: `${y}-${m}-07T19:00:00`, end: `${y}-${m}-07T20:30:00`, type: 'activity', title: '门店活动 - 线上直播专场' },
        { id: 'E015', date: `${y}-${m}-08`, start: `${y}-${m}-08T09:30:00`, end: `${y}-${m}-08T11:00:00`, type: 'meeting',  title: '事业部经营会' },
        { id: 'E016', date: `${y}-${m}-09`, start: `${y}-${m}-09T13:30:00`, end: `${y}-${m}-09T15:00:00`, type: 'ops',     title: '操作会 - 南湖店' },
        { id: 'E017', date: `${y}-${m}-10`, start: `${y}-${m}-10T10:00:00`, end: `${y}-${m}-10T11:30:00`, type: 'training', title: '培训会 - 顾问心法' },
        { id: 'E018', date: `${y}-${m}-11`, start: `${y}-${m}-11T15:00:00`, end: `${y}-${m}-11T17:00:00`, type: 'activity', title: '门店活动 - 西站店沙龙' },
        { id: 'E019', date: `${y}-${m}-12`, start: `${y}-${m}-12T09:30:00`, end: `${y}-${m}-12T11:00:00`, type: 'ops',     title: '操作会 - 融创店' },
        { id: 'E020', date: `${y}-${m}-13`, start: `${y}-${m}-13T14:00:00`, end: `${y}-${m}-13T15:30:00`, type: 'training', title: '培训会 - 经典案例分享' },
        { id: 'E021', date: `${y}-${m}-14`, start: `${y}-${m}-14T19:00:00`, end: `${y}-${m}-14T20:30:00`, type: 'activity', title: '门店活动 - 会员观影会' },
        { id: 'E022', date: `${y}-${m}-15`, start: `${y}-${m}-15T10:00:00`, end: `${y}-${m}-15T12:00:00`, type: 'training', title: '产品培训 - 生命之波' },
        { id: 'E023', date: `${y}-${m}-16`, start: `${y}-${m}-16T09:30:00`, end: `${y}-${m}-16T11:00:00`, type: 'ops',     title: '操作会 - 科技部' },
        { id: 'E024', date: `${y}-${m}-17`, start: `${y}-${m}-17T15:00:00`, end: `${y}-${m}-17T17:00:00`, type: 'activity', title: '门店活动 - 会员答谢宴' },
        { id: 'E025', date: `${y}-${m}-18`, start: `${y}-${m}-18T14:00:00`, end: `${y}-${m}-18T15:30:00`, type: 'training', title: '培训会 - 绩效与辅导' },
        { id: 'E026', date: `${y}-${m}-19`, start: `${y}-${m}-19T10:00:00`, end: `${y}-${m}-19T11:30:00`, type: 'ops',     title: '操作会 - 门店联动' },
        { id: 'E027', date: `${y}-${m}-20`, start: `${y}-${m}-20T09:30:00`, end: `${y}-${m}-20T11:30:00`, type: 'meeting',  title: '季度全体大会' },
        { id: 'E028', date: `${y}-${m}-21`, start: `${y}-${m}-21T19:00:00`, end: `${y}-${m}-21T20:30:00`, type: 'activity', title: '门店活动 - 新品体验会' },
        { id: 'E029', date: `${y}-${m}-22`, start: `${y}-${m}-22T10:00:00`, end: `${y}-${m}-22T11:30:00`, type: 'training', title: '培训会 - 工具使用' },
        { id: 'E030', date: `${y}-${m}-23`, start: `${y}-${m}-23T14:00:00`, end: `${y}-${m}-23T15:30:00`, type: 'ops',     title: '操作会 - 区域复盘' },
        { id: 'E031', date: `${y}-${m}-24`, start: `${y}-${m}-24T15:00:00`, end: `${y}-${m}-24T16:30:00`, type: 'activity', title: '门店活动 - 老客回访日' },
        { id: 'E032', date: `${y}-${m}-25`, start: `${y}-${m}-25T09:30:00`, end: `${y}-${m}-25T11:00:00`, type: 'meeting',  title: '高管经营例会' }
      ],
      calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
      calendarEvents: [],
      calendarHeader: {
        left: 'prev,next today',
        center: 'title',
        right: 'dayGridMonth,timeGridWeek,timeGridDay'
      },
      buttonText: { today: '今日', month: '月', week: '周', day: '日' },
      startTime: null,
      endTime: null,
      calendarHeight: 720,
      typeColorMap: {
        ops: '#6366f1',
        training: '#22c55e',
        meeting: '#f97316',
        activity: '#ec4899'
      }
    }
  },
  computed: {
    visibleProxy: {
      get() {
        return this.visible
      },
      set(v) {
        this.$emit('update:visible', v)
      }
    },
    legendList() {
      return [
        { type: 'ops', label: '操作会', color: this.typeColorMap.ops },
        { type: 'training', label: '培训会', color: this.typeColorMap.training },
        { type: 'meeting', label: '全体大会', color: this.typeColorMap.meeting },
        { type: 'activity', label: '门店活动', color: this.typeColorMap.activity }
      ]
    }
  },
  watch: {
    visible(v) {
      if (v) {
        this.$nextTick(() => {
          this.calcHeight()
          this.initData()
        })
      }
    }
  },
  mounted() {
    window.addEventListener('resize', this.calcHeight)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.calcHeight)
  },
  methods: {
    calcHeight() {
      this.$nextTick(() => {
        const el = this.$el && this.$el.querySelector('.dialog-content')
        if (el) {
          // 提高最小高度,让每天能展示更多事件
          this.calendarHeight = Math.max(el.clientHeight - 20, 650)
        }
      })
    },
    datesRender(info) {
      const view = info.view
      this.startTime = view.activeStart
      this.endTime = view.activeEnd
      this.initData()
    },
    initData() {
      this.loading = true
      setTimeout(() => {
        let filtered = [...this.companyEvents]
        if (this.startTime && this.endTime) {
          filtered = filtered.filter(e => {
            const t = new Date(e.start).getTime()
            return t >= this.startTime.getTime() && t < this.endTime.getTime()
          })
        }
        this.calendarEvents = filtered.map(evt => {
          const color = this.typeColorMap[evt.type] || '#3b82f6'
          return {
            id: evt.id,
            title: evt.title,
            start: new Date(evt.start).toISOString(),
            end: new Date(evt.end).toISOString(),
            color,
            editable: false,
            allDay: false,
            extendedProps: {
              type: evt.type
            }
          }
        })
        this.loading = false
      }, 200)
    },
    handleEventClick(info) {
      const evt = info && info.event
      if (!evt) return
      const raw = this.companyEvents.find(e => e.id === evt.id) || {}
      const start = evt.start
      const end = evt.end
      const fmt = d => {
        if (!d) return ''
        const hh = String(d.getHours()).padStart(2, '0')
        const mm = String(d.getMinutes()).padStart(2, '0')
        return `${hh}:${mm}`
      }
      const timeRange = start ? `${fmt(start)} - ${fmt(end || start)}` : ''
      const type = (evt.extendedProps && evt.extendedProps.type) || ''
      const typeText = this.legendList.find(x => x.type === type)?.label || '安排'
      const dateStr = start ? this.formatDate(start) : ''
      const subject = raw.title || evt.title || '—'
      const content = raw.desc || '—'
      const otherParts = []
      if (raw.store) otherParts.push(`门店/地点:${raw.store}`)
      otherParts.push(`类型:${typeText}`)
      const otherText = otherParts.join(';')

      const html = `
        <div style="line-height:1.7;font-size:13px;color:#4b5563;">
          <div><strong>主题:</strong>${subject}</div>
          <div><strong>内容:</strong>${content}</div>
          <div><strong>时间:</strong>${dateStr} ${timeRange}</div>
          <div><strong>其他:</strong>${otherText}</div>
        </div>
      `
      this.$alert(html, '日程详情', {
        confirmButtonText: '知道了',
        dangerouslyUseHTMLString: true
      })
    }
  }
}
</script>

<style lang="scss">
@import '~@fullcalendar/core/main.css';
@import '~@fullcalendar/daygrid/main.css';
@import '~@fullcalendar/timegrid/main.css';

.company-calendar-dialog .company-calendar {
  .fc-toolbar.fc-header-toolbar {
    padding: 16px 20px;
    margin-bottom: 0;
    border-bottom: 2px solid #f1f5f9;
    background: linear-gradient(135deg, rgba(59,130,246,0.02) 0%, rgba(96,165,250,0.02) 100%);
  }
  .fc-toolbar-title {
    font-size: 18px;
    font-weight: 700;
    color: #1e293b;
  }
  .fc-button-primary {
    background-color: #3b82f6;
    border-color: #3b82f6;
    border-radius: 10px;
    font-size: 13px;
    font-weight: 500;
    height: 34px;
    line-height: 34px;
    padding: 0 14px;
    transition: all 0.2s;
    &:hover {
      background: #2563eb;
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(59,130,246,0.3);
    }
  }
  .fc-button-primary:not(:disabled):active,
  .fc-button-primary:not(:disabled).fc-button-active {
    background-color: #2563eb;
    border-color: #2563eb;
    box-shadow: 0 4px 12px rgba(59,130,246,0.3);
  }
  .fc-day-today {
    background: linear-gradient(135deg, rgba(59,130,246,0.05) 0%, rgba(96,165,250,0.05) 100%);
    .fc-daygrid-day-number {
      background: linear-gradient(135deg, #3b82f6, #2563eb);
      color: #fff;
      border-radius: 6px;
      box-shadow: 0 2px 8px rgba(59,130,246,0.3);
    }
  }
  .fc-event {
    cursor: pointer;
    border-radius: 6px;
    padding: 3px 6px;
    font-size: 12px;
    font-weight: 500;
    border: none;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    transition: all 0.2s;
    &:hover {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    }
  }
  .fc-day-header {
    font-size: 14px !important;
    color: #64748b !important;
    font-weight: 700 !important;
    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
    border-bottom: 2px solid #e2e8f0 !important;
    padding: 12px 8px !important;
  }
  .fc-unthemed th,
  .fc-unthemed td {
    border-color: #f1f5f9;
  }
}
</style>

<style lang="scss" scoped>
::v-deep .company-calendar-dialog {
  max-width: 1600px;
  margin-top: 3vh !important;
  border-radius: 20px;
  padding: 0;
  background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%);
  box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9);
  backdrop-filter: blur(22px);
  -webkit-backdrop-filter: blur(22px);
}
::v-deep .el-dialog__header { display: none; }
::v-deep .el-dialog__body { padding: 0; }

.dialog-inner {
  display: flex;
  flex-direction: column;
  max-height: 92vh;
  height: 88vh;
}

.dialog-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 18px 22px 0;
  padding: 10px 14px;
  border-radius: 14px;
  background: rgba(219,234,254,0.96);
}

.dialog-title {
  font-size: 17px;
  font-weight: 600;
  color: #0f172a;
}

.dialog-legend {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: #6b7280;
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 999px;
}

.dialog-close {
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #64748b;
  transition: all 0.15s;
  &:hover {
    background: rgba(0,0,0,0.06);
    color: #0f172a;
  }
}

.dialog-content {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  padding: 0 22px 14px;
}
</style>