mall-appointment-dialog.vue 9.77 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="920px"
    :close-on-click-modal="false"
    custom-class="mall-appointment-dialog"
    append-to-body
    @open="handleOpen"
  >
    <div class="dialog-inner">
      <div class="dialog-header">
        <div class="dialog-title-wrap">
          <div class="dialog-title">会员商城预约</div>
          <div class="dialog-subtitle">本门店会员在商城提交的到店预约,请电话确认后再填写正式预约记录</div>
        </div>
        <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div class="dialog-body">
        <div class="filter-bar">
          <el-radio-group v-model="statusFilter" size="small" @change="handleQuery">
            <el-radio-button :label="''">全部</el-radio-button>
            <el-radio-button :label="1">待确认</el-radio-button>
            <el-radio-button :label="2">已确认</el-radio-button>
            <el-radio-button :label="3">已取消</el-radio-button>
          </el-radio-group>
          <div class="filter-right">
            <el-input v-model="keyword" placeholder="会员昵称/手机号" clearable size="small" class="filter-input"
              @keyup.enter.native="handleQuery" @clear="handleQuery" />
            <el-button type="primary" icon="el-icon-search" size="small" :loading="loading" @click="handleQuery">查询</el-button>
          </div>
        </div>

        <el-table
          v-loading="loading"
          :data="list"
          border
          size="small"
          :header-cell-style="{ background: '#f8fafc', color: '#64748b', fontWeight: 600 }"
        >
          <el-table-column label="会员" min-width="110" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ emptyText(row.memberName) }}</template>
          </el-table-column>
          <el-table-column label="手机号" width="130" align="center">
            <template slot-scope="{ row }">{{ emptyText($maskMobile(row.memberPhone)) }}</template>
          </el-table-column>
          <el-table-column label="期望到店" width="160" align="center">
            <template slot-scope="{ row }">{{ formatDate(row.appointDate) }} {{ row.timeSlot || '' }}</template>
          </el-table-column>
          <el-table-column label="需求" min-width="150" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ emptyText(row.projectDesc) }}</template>
          </el-table-column>
          <el-table-column label="备注" min-width="120" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ emptyText(row.remark) }}</template>
          </el-table-column>
          <el-table-column label="状态" width="84" align="center">
            <template slot-scope="{ row }">
              <el-tag size="mini" :type="statusType(row.status)" effect="plain">{{ statusText(row.status) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column label="提交时间" width="150" align="center">
            <template slot-scope="{ row }">{{ formatDateTime(row.createTime) }}</template>
          </el-table-column>
          <el-table-column label="操作" width="140" align="center" fixed="right">
            <template slot-scope="{ row }">
              <el-button v-if="row.status === 1" type="text" size="mini" @click="handleConfirm(row)">确认</el-button>
              <el-button v-if="row.status !== 3" type="text" size="mini" class="danger-text" @click="handleCancel(row)">取消</el-button>
              <span v-if="row.status === 2" class="confirmed-tip">{{ emptyText(row.confirmUserName) }}</span>
            </template>
          </el-table-column>
        </el-table>
        <div v-if="!loading && !list.length" class="empty-tip">暂无会员预约</div>

        <div class="pager" v-if="total > 0">
          <el-pagination
            background
            layout="total, prev, pager, next"
            :total="total"
            :current-page.sync="currentPage"
            :page-size="pageSize"
            @current-change="loadList"
          />
        </div>
      </div>

      <div class="dialog-footer">
        <el-button @click="visibleProxy = false">关闭</el-button>
        <span class="footer-tip">确认后请到「新建预约」填写正式预约记录</span>
      </div>
    </div>
  </el-dialog>
</template>

<script>
import { getStoreAppointmentList, confirmAppointment, cancelAppointment } from '@/api/mall'

const STATUS_MAP = { 1: '待确认', 2: '已确认', 3: '已取消' }

export default {
  name: 'MallAppointmentDialog',
  props: {
    visible: { type: Boolean, default: false },
    storeId: { type: String, default: '' }
  },
  data() {
    return {
      statusFilter: '',
      keyword: '',
      list: [],
      loading: false,
      total: 0,
      currentPage: 1,
      pageSize: 10
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    }
  },
  methods: {
    resolveStoreId() {
      return this.storeId || localStorage.getItem('store_current_store_id') || ''
    },
    handleOpen() {
      this.statusFilter = ''
      this.keyword = ''
      this.currentPage = 1
      this.loadList()
    },
    handleQuery() {
      this.currentPage = 1
      this.loadList()
    },
    loadList() {
      const storeId = this.resolveStoreId()
      if (!storeId) {
        this.$message.warning('未获取到当前门店信息')
        return
      }
      this.loading = true
      const params = {
        storeId,
        currentPage: this.currentPage,
        pageSize: this.pageSize
      }
      if (this.statusFilter !== '' && this.statusFilter !== null) params.status = this.statusFilter
      if (this.keyword) params.keyword = this.keyword
      getStoreAppointmentList(params).then(res => {
        const data = res.data || {}
        this.list = data.list || []
        this.total = data.pagination ? data.pagination.total : 0
        this.loading = false
      }).catch(() => {
        this.list = []
        this.total = 0
        this.loading = false
      })
    },
    handleConfirm(row) {
      this.$prompt('电话确认后填写沟通结果(可选),确认后标记为已确认', '确认会员预约', {
        confirmButtonText: '确认预约',
        cancelButtonText: '关闭',
        inputPlaceholder: '如已约定到店时间'
      }).then(({ value }) => {
        confirmAppointment({ id: row.id, storeId: this.resolveStoreId(), confirmRemark: value }).then(res => {
          this.$message.success(res.msg || '确认成功')
          this.loadList()
        })
      }).catch(() => {})
    },
    handleCancel(row) {
      this.$prompt('请输入取消原因(可选)', '取消会员预约', {
        confirmButtonText: '确定',
        cancelButtonText: '关闭',
        inputPlaceholder: '取消原因'
      }).then(({ value }) => {
        cancelAppointment({ id: row.id, storeId: this.resolveStoreId(), confirmRemark: value }).then(res => {
          this.$message.success(res.msg || '已取消')
          this.loadList()
        })
      }).catch(() => {})
    },
    emptyText(val) {
      if (val === undefined || val === null || String(val).trim() === '') return '无'
      return val
    },
    statusText(status) {
      return STATUS_MAP[Number(status)] || '无'
    },
    statusType(status) {
      return { 1: 'warning', 2: 'success', 3: 'info' }[Number(status)] || 'info'
    },
    formatDate(val) {
      if (!val) return '无'
      return String(val).substring(0, 10)
    },
    formatDateTime(val) {
      if (!val) return '无'
      const d = new Date(val)
      if (isNaN(d.getTime())) return String(val)
      const y = d.getFullYear()
      const m = String(d.getMonth() + 1).padStart(2, '0')
      const day = String(d.getDate()).padStart(2, '0')
      const hh = String(d.getHours()).padStart(2, '0')
      const mm = String(d.getMinutes()).padStart(2, '0')
      return `${y}-${m}-${day} ${hh}:${mm}`
    }
  }
}
</script>

<style lang="scss" scoped>
::v-deep .mall-appointment-dialog {
  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);
  .el-dialog__header { display: none; }
  .el-dialog__body { padding: 0; }
}
.dialog-inner { display: flex; flex-direction: column; max-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-wrap { display: flex; flex-direction: column; gap: 2px; }
.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; }
.dialog-subtitle { font-size: 12px; color: #64748b; }
.dialog-close {
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #64748b;
  &:hover { background: rgba(0, 0, 0, 0.06); color: #0f172a; }
}
.dialog-body { flex: 1; min-height: 0; overflow: auto; padding: 16px 22px 4px; }
.filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.filter-right { display: flex; align-items: center; gap: 8px; }
.filter-input { width: 200px; }
.danger-text { color: #f56c6c; }
.confirmed-tip { font-size: 12px; color: #94a3b8; }
.empty-tip { text-align: center; color: #94a3b8; font-size: 13px; padding: 32px 0; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.dialog-footer {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 22px 18px;
  border-top: 1px solid rgba(229, 231, 235, 0.6);
}
.footer-tip { font-size: 12px; color: #f59e0b; }
</style>