room-usage-dialog.vue 6.7 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="860px"
    :close-on-click-modal="false"
    custom-class="room-usage-dialog"
    append-to-body
  >
    <div class="inner">
      <div class="header">
        <div class="title">房态 · {{ date || '—' }}</div>
        <span class="close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div class="body">
        <div class="legend">
          <span class="legend-item"><i class="dot dot--free"></i>空闲</span>
          <span class="legend-item"><i class="dot dot--busy"></i>占用</span>
          <span class="legend-item"><i class="dot dot--slot"></i>30分钟/格</span>
          <span class="legend-tip">当前为 mock 房态,用于前端闭环演示</span>
        </div>

        <div class="table">
          <div class="thead">
            <div class="cell cell--room">房间</div>
            <div class="cell cell--grid">
              <div class="grid">
                <div v-for="i in 48" :key="i" class="t">
                  {{ (i - 1) % 2 === 0 ? formatSlotTime(i - 1) : '' }}
                </div>
              </div>
            </div>
          </div>

          <div v-for="r in normalizedRooms" :key="r.id" class="row">
            <div class="cell cell--room">
              <div class="room-name">{{ r.name }}</div>
              <div class="room-sub">今日占用 {{ countBusySlots(r.id) }} 格</div>
            </div>
            <div class="cell cell--grid">
              <div class="grid">
                <div
                  v-for="i in 48"
                  :key="i"
                  :class="['slot', isBusy(r.id, i - 1) ? 'slot--busy' : 'slot--free']"
                  :title="slotTitle(r.id, i - 1)"
                ></div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="footer">
        <el-button size="small" @click="visibleProxy = false">关 闭</el-button>
      </div>
    </div>
  </el-dialog>
</template>

<script>
export default {
  name: 'RoomUsageDialog',
  props: {
    visible: { type: Boolean, default: false },
    date: { type: String, default: '' },
    rooms: { type: Array, default: () => [] }
  },
  data() {
    return {
      // mock:key = roomId, value = [{ startSlot, endSlot, label }]
      mockBusy: {
        R001: [{ startSlot: 18, endSlot: 22, label: '预约占用' }, { startSlot: 30, endSlot: 34, label: '清洁消杀' }],
        R002: [{ startSlot: 20, endSlot: 24, label: '预约占用' }],
        R003: [{ startSlot: 26, endSlot: 28, label: '设备维护' }],
        R004: [{ startSlot: 32, endSlot: 40, label: 'VIP预约' }]
      }
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    normalizedRooms() {
      if (this.rooms && this.rooms.length) return this.rooms
      return [
        { id: 'R001', name: '1号房' },
        { id: 'R002', name: '2号房' },
        { id: 'R003', name: '3号房' },
        { id: 'R004', name: 'VIP房' }
      ]
    }
  },
  methods: {
    formatSlotTime(slotIndex) {
      const h = Math.floor(slotIndex / 2)
      const m = (slotIndex % 2) * 30
      return `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`
    },
    isBusy(roomId, slotIndex) {
      const list = this.mockBusy[roomId] || []
      return list.some(x => slotIndex >= x.startSlot && slotIndex < x.endSlot)
    },
    slotTitle(roomId, slotIndex) {
      const time = this.formatSlotTime(slotIndex)
      const list = this.mockBusy[roomId] || []
      const hit = list.find(x => slotIndex >= x.startSlot && slotIndex < x.endSlot)
      if (!hit) return `${time} 空闲`
      return `${time} ${hit.label}`
    },
    countBusySlots(roomId) {
      const list = this.mockBusy[roomId] || []
      return list.reduce((sum, x) => sum + Math.max((x.endSlot - x.startSlot), 0), 0)
    }
  }
}
</script>

<style lang="scss" scoped>
.inner {
  padding: 18px 22px 14px;
}

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

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

.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; }
}

.legend {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  font-size: 12px;
  color: #64748b;
  margin-bottom: 10px;
}

.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-tip { margin-left: auto; color: #94a3b8; }

.dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.dot--free { background: rgba(148, 163, 184, 0.35); }
.dot--busy { background: rgba(239, 68, 68, 0.55); }
.dot--slot { background: #e2e8f0; }

.table {
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  overflow: hidden;
  background: #fff;
}

.thead, .row {
  display: grid;
  grid-template-columns: 140px 1fr;
}

.cell {
  padding: 10px 10px;
  border-right: 1px solid #f1f5f9;
  border-bottom: 1px solid #f1f5f9;
}

.cell--room {
  background: #f8fafc;
}

.room-name {
  font-size: 14px;
  font-weight: 600;
  color: #111827;
}

.room-sub {
  font-size: 11px;
  color: #94a3b8;
  margin-top: 2px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(48, 1fr);
  gap: 0;
  min-height: 24px;
}

.t {
  font-size: 9px;
  color: #94a3b8;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
}

.slot {
  min-height: 18px;
  background: #f8fafc;
}

.slot--free {
  background: rgba(148, 163, 184, 0.18);
}

.slot--busy {
  background: rgba(239, 68, 68, 0.5);
}

.footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid #f1f5f9;
}

::v-deep .room-usage-dialog {
  max-width: 860px;
  margin-top: 7vh !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 .room-usage-dialog .el-dialog__header { display: none; }
::v-deep .room-usage-dialog .el-dialog__body { padding: 0; }
::v-deep .room-usage-dialog .el-button--default {
  border-radius: 999px;
  padding: 0 18px;
  height: 30px;
  line-height: 30px;
  background: rgba(239, 246, 255, 0.9);
  color: #2563eb;
  border-color: rgba(37, 99, 235, 0.18);
  font-size: 12px;
}
</style>