booking-consume-detail-dialog.vue 8.99 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="700px"
    :close-on-click-modal="false"
    custom-class="booking-consume-detail-dialog"
    append-to-body
  >
    <div v-if="booking" class="inner">
      <div class="header">
        <div class="title">预约消耗单详情</div>
        <span class="close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div class="body">
        <div class="row"><span class="label">会员</span><span class="value">{{ booking.memberName || '无' }}</span></div>
        <div class="row"><span class="label">预约日期</span><span class="value">{{ booking.date || '无' }}</span></div>
        <div class="row"><span class="label">开始/结束</span><span class="value">{{ timeRangeText }}</span></div>
        <div class="row"><span class="label">房间</span><span class="value">{{ booking.roomName || '无' }}</span></div>
        <div class="row">
          <span class="label">健康师</span>
          <span class="value">
            <span v-if="therapistNames.length">{{ therapistNames.join('、') }}</span>
            <span v-else>无</span>
          </span>
        </div>
        <div class="row row--items">
          <span class="label">品项明细</span>
          <div class="value value--block">
            <div v-if="displayItems.length" class="items-list">
              <div v-for="(it, idx) in displayItems" :key="idx" class="item-line">
                <div class="item-main">
                  <span class="item-name">{{ it.label || '无' }}</span>
                  <span class="item-count">×{{ it.count }}</span>
                </div>
                <div class="item-sub">
                  <span class="sub-label">服务健康师:</span>
                  <span class="sub-value">{{ it.workerNamesText }}</span>
                </div>
              </div>
            </div>
            <div v-else class="empty-text">无</div>
          </div>
        </div>
        <div class="row">
          <span class="label">状态</span>
          <span :class="['value', 'status', 'status--' + (booking.status || 'booked')]">{{ statusText }}</span>
        </div>
        <div class="row row--remark">
          <span class="label">备注</span>
          <span class="value value--multiline">{{ booking.remark || '无' }}</span>
        </div>
      </div>

      <div class="footer">
        <el-button size="small" @click="visibleProxy = false">关 闭</el-button>
        <el-button
          v-if="booking.status !== 'cancelled' && booking.status !== 'converted'"
          size="small"
          class="danger-btn"
          @click="$emit('cancel', booking)"
        >
          取消预约
        </el-button>
        <el-button
          v-if="booking.status !== 'cancelled' && booking.status !== 'converted'"
          type="primary"
          plain
          size="small"
          @click="$emit('edit', booking)"
        >
          修改预约
        </el-button>
        <el-button
          v-if="booking.status === 'booked'"
          type="primary"
          size="small"
          @click="$emit('start', booking)"
        >
          开始服务
        </el-button>
        <el-button
          v-if="booking.status !== 'converted'"
          type="primary"
          plain
          size="small"
          @click="$emit('convert', booking)"
        >
          转消耗开单
        </el-button>
      </div>
    </div>
  </el-dialog>
</template>

<script>
export default {
  name: 'BookingConsumeDetailDialog',
  props: {
    visible: { type: Boolean, default: false },
    booking: { type: Object, default: null }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    timeRangeText() {
      const b = this.booking || {}
      const st = b.startTime || '无'
      const et = b.endTime || '无'
      if (st === '无' && et === '无') return '无'
      return `${st}-${et}`
    },
    therapistNames() {
      const b = this.booking || {}
      if (Array.isArray(b.therapistNames) && b.therapistNames.length) return b.therapistNames
      return []
    },
    therapistNameMap() {
      const b = this.booking || {}
      const ids = Array.isArray(b.therapistIds) ? b.therapistIds : []
      const names = Array.isArray(b.therapistNames) ? b.therapistNames : []
      const map = new Map()
      ids.forEach((id, idx) => {
        if (!id) return
        map.set(id, names[idx] || id)
      })
      return map
    },
    displayItems() {
      const b = this.booking || {}
      const list = Array.isArray(b.items) ? b.items : []
      const map = this.therapistNameMap
      return list.map(x => {
        const count = (x && (x.count != null ? x.count : x.qty)) || 1
        const workers = Array.isArray(x && x.workers) ? x.workers : []
        const workerIds = workers.map(w => w && w.workerId).filter(Boolean)
        const workerNames = workerIds.map(id => map.get(id) || id)
        return {
          label: (x && x.label) || '',
          count,
          workerNamesText: workerNames.length ? workerNames.join('、') : '无'
        }
      })
    },
    statusText() {
      const s = (this.booking && this.booking.status) || 'booked'
      const map = {
        booked: '已预约',
        serving: '服务中',
        converted: '已转消耗',
        cancelled: '已取消'
      }
      return map[s] || '已预约'
    }
  }
}
</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; }
}

.body {
  padding: 4px 0 16px;
}

.row {
  display: flex;
  padding: 10px 0;
  border-bottom: 1px solid #f1f5f9;
  font-size: 14px;
}

.row--items {
  align-items: flex-start;
}

.label {
  width: 96px;
  color: #64748b;
  flex-shrink: 0;
}

.value {
  flex: 1;
  min-width: 0;
  color: #111827;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.5;
  word-break: break-all;
}

.value--block {
  white-space: normal;
  overflow: visible;
}

.value--multiline {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.5;
  word-break: break-all;
}

.items-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.item-line {
  padding: 10px 12px;
  border: 1px solid rgba(226, 232, 240, 0.9);
  border-radius: 12px;
  background: rgba(248, 250, 252, 0.75);
}

.item-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.item-name {
  font-weight: 600;
  color: #0f172a;
  flex: 1;
  min-width: 0;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.4;
  word-break: break-all;
}

.item-count {
  flex-shrink: 0;
  font-size: 12px;
  color: #2563eb;
  font-weight: 700;
}

.item-sub {
  margin-top: 6px;
  font-size: 12px;
  color: #64748b;
  display: flex;
  gap: 6px;
  line-height: 1.4;
}

.sub-label {
  flex-shrink: 0;
}

.sub-value {
  flex: 1;
  min-width: 0;
  word-break: break-all;
}

.empty-text {
  color: #94a3b8;
  font-size: 13px;
}

.status {
  font-weight: 600;
}
.status--booked { color: #2563eb; }
.status--serving { color: #f97316; }
.status--converted { color: #16a34a; }
.status--cancelled { color: #ef4444; }

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

.danger-btn {
  border-radius: 999px !important;
  background: rgba(254, 226, 226, 0.8) !important;
  color: #b91c1c !important;
  border-color: rgba(239, 68, 68, 0.25) !important;
}

::v-deep .booking-consume-detail-dialog {
  max-width: 700px;
  margin-top: 10vh !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 .booking-consume-detail-dialog .el-dialog__header { display: none; }
::v-deep .booking-consume-detail-dialog .el-dialog__body { padding: 0; }
::v-deep .booking-consume-detail-dialog .el-button--primary {
  border-radius: 999px;
  padding: 0 18px;
  height: 30px;
  line-height: 30px;
  background: #2563eb;
  border-color: #2563eb;
  box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35);
  font-size: 12px;
}
::v-deep .booking-consume-detail-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>