room-usage-dialog.vue 8.43 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" v-loading="loading">
        <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 v-if="loadError" class="legend-tip legend-tip--err">{{ loadError }}</span>
          <span v-else-if="!normalizedRooms.length" class="legend-tip">当前门店暂无房间,请先在管理端配置</span>
        </div>

        <div v-if="normalizedRooms.length" 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>
import { getYyjlList } from '@/api/lqYyjl'
import { getRoomsByStoreId } from '@/api/lqMdRoom'
import {
  buildYysjTimestampRange,
  buildRoomBusyMap,
  parseDayRangeYmd
} from '@/utils/appointmentHelper'

export default {
  name: 'RoomUsageDialog',
  props: {
    visible: { type: Boolean, default: false },
    date: { type: String, default: '' },
    storeId: { type: String, default: '' },
    rooms: { type: Array, default: () => [] }
  },
  data() {
    return {
      loading: false,
      loadError: '',
      localRooms: [],
      roomBusy: {}
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    normalizedRooms() {
      const src = (this.rooms && this.rooms.length) ? this.rooms : this.localRooms
      return (src || []).map(r => ({
        id: String(r.id),
        name: r.name || r.roomName || '房间'
      }))
    }
  },
  watch: {
    visible(v) {
      if (v) this.loadOccupancy()
    },
    date() {
      if (this.visible) this.loadOccupancy()
    },
    storeId() {
      if (this.visible) this.loadOccupancy()
    }
  },
  methods: {
    resolveStoreId() {
      return this.storeId || (this.$store.getters.storeInfo || {}).storeId || ''
    },
    async loadOccupancy() {
      const storeId = this.resolveStoreId()
      const dayRange = parseDayRangeYmd(this.date)
      if (!storeId) {
        this.loadError = '未获取到当前门店'
        this.roomBusy = {}
        return
      }
      if (!dayRange) {
        this.loadError = '日期无效'
        this.roomBusy = {}
        return
      }

      this.loading = true
      this.loadError = ''
      try {
        if (!this.rooms || !this.rooms.length) {
          const roomRes = await getRoomsByStoreId(storeId)
          const list = (roomRes.data && roomRes.data.list) || []
          this.localRooms = list
            .filter(r => r.enabled !== 0 && r.enabled !== '0' && r.enabled !== false)
            .map(r => ({ id: r.id, name: r.roomName }))
        } else {
          this.localRooms = []
        }

        const yysj = buildYysjTimestampRange(dayRange.dayStart, dayRange.dayEnd)
        const res = await getYyjlList({
          djmd: storeId,
          currentPage: 1,
          pageSize: 500,
          sidx: 'yysj',
          sort: 'asc',
          yysj
        })
        const bookings = (res.data && res.data.list) || []
        this.roomBusy = buildRoomBusyMap(bookings)
      } catch (e) {
        this.roomBusy = {}
        this.loadError = '加载房态失败,请稍后重试'
      } finally {
        this.loading = false
      }
    },
    formatSlotTime(slotIndex) {
      const h = Math.floor(slotIndex / 2)
      const m = (slotIndex % 2) * 30
      return `${String(h).padStart(2, '0')}:${m === 0 ? '00' : '30'}`
    },
    getBusyList(roomId) {
      return this.roomBusy[String(roomId)] || []
    },
    isBusy(roomId, slotIndex) {
      return this.getBusyList(roomId).some(x => slotIndex >= x.startSlot && slotIndex < x.endSlot)
    },
    slotTitle(roomId, slotIndex) {
      const time = this.formatSlotTime(slotIndex)
      const hit = this.getBusyList(roomId).find(x => slotIndex >= x.startSlot && slotIndex < x.endSlot)
      if (!hit) return `${time} 空闲`
      return `${time} ${hit.label}`
    },
    countBusySlots(roomId) {
      return this.getBusyList(roomId).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; }
.legend-tip--err { color: #f56c6c; }

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