appointment-detail-dialog.vue 11.5 KB
<template>
  <el-dialog
    title="预约详情"
    :visible.sync="visibleProxy"
    :close-on-click-modal="false"
    class="NCC-dialog NCC-dialog_center appointment-detail-dialog"
    lock-scroll
    width="860px"
    append-to-body
  >
    <div class="detail-content" v-loading="loading">
      <template v-if="booking">
        <el-card class="info-card" shadow="hover">
          <div slot="header" class="card-header">
            <i class="el-icon-date"></i>
            <span class="card-title">基本信息</span>
          </div>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">单据门店:</label>
                <span class="info-value">{{ storeDisplay }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">预约状态:</label>
                <el-tag :type="statusTagType" size="small">{{ statusDisplay }}</el-tag>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">顾客姓名:</label>
                <span class="info-value">{{ booking.memberName || '无' }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">沟通人:</label>
                <span class="info-value">{{ booking.yyrName || '无' }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">预约开始:</label>
                <span class="info-value">{{ startTimeText }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">预约结束:</label>
                <span class="info-value">{{ endTimeText }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">房间:</label>
                <span class="info-value">{{ booking.roomName || '无' }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <label class="info-label">健康师:</label>
                <span class="info-value">
                  <span v-if="therapistNames.length">{{ therapistNames.join('、') }}</span>
                  <span v-else>无</span>
                </span>
              </div>
            </el-col>
            <el-col :span="24">
              <div class="info-item">
                <label class="info-label">备注:</label>
                <span class="info-value">{{ booking.remark || '无' }}</span>
              </div>
            </el-col>
            <el-col :span="24">
              <div class="info-item">
                <label class="info-label">关联沟通号:</label>
                <span class="info-value">
                  <el-link
                    v-if="booking.InviteId"
                    type="primary"
                    :underline="false"
                    @click="goToYaoyjl"
                  >
                    {{ booking.InviteId }}
                  </el-link>
                  <span v-else>无</span>
                </span>
              </div>
            </el-col>
          </el-row>
        </el-card>

        <el-card class="info-card" shadow="hover">
          <div slot="header" class="card-header">
            <i class="el-icon-goods"></i>
            <span class="card-title">预约项目</span>
          </div>
          <el-table
            v-if="displayItems.length"
            :data="displayItems"
            border
            size="small"
            style="width: 100%"
          >
            <el-table-column prop="label" label="项目名称" min-width="180" show-overflow-tooltip />
            <el-table-column prop="count" label="次数" width="80" align="center" />
            <el-table-column prop="workerNamesText" label="服务健康师" min-width="160" show-overflow-tooltip />
          </el-table>
          <div v-else class="empty-block">无</div>
        </el-card>

        <el-card v-if="relatedConsumes.length" class="info-card" shadow="hover">
          <div slot="header" class="card-header">
            <i class="el-icon-tickets"></i>
            <span class="card-title">关联耗卡</span>
          </div>
          <el-table :data="relatedConsumes" border size="small" style="width: 100%">
            <el-table-column prop="id" label="耗卡编号" min-width="160" show-overflow-tooltip />
            <el-table-column label="耗卡时间" width="160">
              <template slot-scope="scope">{{ formatConsumeTime(scope.row.hksj) }}</template>
            </el-table-column>
            <el-table-column prop="hymc" label="会员" width="100" show-overflow-tooltip />
            <el-table-column label="消费金额" width="110">
              <template slot-scope="scope">{{ formatMoney(scope.row.xfje) }}</template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template slot-scope="scope">
                <el-button type="text" @click="openConsumeDetail(scope.row.id)">详情</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </template>
    </div>

    <LqXhHyhkDetail ref="consumeDetailDialog" />

    <span slot="footer" class="dialog-footer">
      <el-button @click="visibleProxy = false">关 闭</el-button>
    </span>
  </el-dialog>
</template>

<script>
import request from '@/utils/request'
import LqXhHyhkDetail from '@/views/lqXhHyhk/detail'
import { mapYyjlInfoToFormRecord, statusText, apiStatusToUi } from '@/utils/appointmentHelper'

export default {
  name: 'AppointmentDetailDialog',
  components: { LqXhHyhkDetail },
  props: {
    visible: { type: Boolean, default: false },
    bookingId: { type: String, default: '' },
    fallbackBooking: { type: Object, default: null },
    storeOptions: { type: Array, default: () => [] }
  },
  data() {
    return {
      loading: false,
      booking: null
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    storeDisplay() {
      const b = this.booking || {}
      if (b.storeName && b.storeName !== '无') return b.storeName
      const id = b.storeId
      if (id && this.storeOptions.length) {
        const found = this.storeOptions.find(x => x.id === id)
        if (found) return found.fullName
      }
      return b.storeName || '无'
    },
    startTimeText() {
      const b = this.booking || {}
      if (!b.date && !b.startTime) return '无'
      if (!b.startTime || b.startTime === '无') return b.date || '无'
      return `${b.date || ''} ${b.startTime}`.trim()
    },
    endTimeText() {
      const b = this.booking || {}
      if (!b.date && !b.endTime) return '无'
      if (!b.endTime || b.endTime === '无') return b.date || '无'
      return `${b.date || ''} ${b.endTime}`.trim()
    },
    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('、') : '无'
        }
      })
    },
    statusDisplay() {
      const b = this.booking || {}
      return statusText(b.fStatus || b.status)
    },
    statusTagType() {
      const b = this.booking || {}
      const ui = apiStatusToUi(b.fStatus || b.status)
      if (ui === 'booked') return 'primary'
      if (ui === 'serving') return 'success'
      if (ui === 'converted') return 'info'
      if (ui === 'cancelled') return 'danger'
      return 'info'
    },
    relatedConsumes() {
      const b = this.booking || {}
      if (Array.isArray(b.relatedConsumes) && b.relatedConsumes.length) {
        return b.relatedConsumes
      }
      if (b.consumeId) {
        return [{ id: b.consumeId, hksj: null, xfje: null, hymc: b.memberName || '无' }]
      }
      return []
    }
  },
  watch: {
    visible(v) {
      if (v && this.bookingId) {
        this.loadDetail()
      } else if (!v) {
        this.booking = null
      }
    },
    bookingId(id) {
      if (this.visibleProxy && id) {
        this.loadDetail()
      }
    }
  },
  methods: {
    loadDetail() {
      if (!this.bookingId) return
      this.loading = true
      request({
        url: `/api/Extend/LqYyjl/${this.bookingId}`,
        method: 'GET'
      }).then(res => {
        const info = res.data || {}
        this.booking = mapYyjlInfoToFormRecord(info, info.lqYyjlPxList || [])
      }).catch(() => {
        this.booking = this.fallbackBooking ? { ...this.fallbackBooking } : null
        if (!this.booking) {
          this.$message.error('加载预约详情失败')
        }
      }).finally(() => {
        this.loading = false
      })
    },
    goToYaoyjl() {
      const id = this.booking && this.booking.InviteId
      if (!id) return
      this.visibleProxy = false
      this.$router.push({
        path: '/lqYaoyjl',
        query: { id }
      })
    },
    openConsumeDetail(consumeId) {
      if (!consumeId || !this.$refs.consumeDetailDialog) return
      this.$refs.consumeDetailDialog.init(consumeId)
    },
    formatConsumeTime(val) {
      if (!val) return '无'
      if (this.ncc && this.ncc.toDate) {
        return this.ncc.toDate(val, 'yyyy-MM-dd HH:mm:ss')
      }
      return String(val)
    },
    formatMoney(val) {
      if (val === null || val === undefined || val === '') return '无'
      const num = Number(val)
      if (Number.isNaN(num)) return '无'
      return '¥' + num.toFixed(2)
    }
  }
}
</script>

<style lang="scss" scoped>
.detail-content {
  max-height: 65vh;
  overflow-y: auto;
}

.info-card {
  margin-bottom: 16px;
  border-radius: 8px;

  &:last-child {
    margin-bottom: 0;
  }
}

.card-header {
  display: flex;
  align-items: center;
  gap: 8px;

  i {
    color: #409EFF;
    font-size: 16px;
  }
}

.card-title {
  font-size: 15px;
  font-weight: 600;
  color: #303133;
}

.info-item {
  display: flex;
  align-items: flex-start;
  margin-bottom: 14px;
  min-height: 22px;
  line-height: 22px;
}

.info-label {
  flex-shrink: 0;
  width: 96px;
  color: #909399;
  font-size: 14px;
}

.info-value {
  flex: 1;
  min-width: 0;
  color: #606266;
  font-size: 14px;
  word-break: break-all;
}

.empty-block {
  padding: 24px 0;
  text-align: center;
  color: #909399;
  font-size: 14px;
}

.dialog-footer {
  text-align: left;
}
</style>