BillingListDialog.vue 17.4 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="90%"
    :close-on-click-modal="false"
    custom-class="billing-list-dialog"
    append-to-body
  >
    <div class="dialog-inner">
      <div class="dialog-header">
        <div class="dialog-title">开单记录</div>
        <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div class="dialog-search">
        <el-form @submit.native.prevent :inline="true" size="small">
          <el-form-item label="开单日期">
            <el-date-picker v-model="query.kdrq" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" />
          </el-form-item>
          <el-form-item label="开单会员">
            <el-select v-model="query.kdhy" filterable remote reserve-keyword clearable placeholder="搜索会员" :remote-method="searchMember" :loading="memberLoading" style="width:200px">
              <el-option v-for="m in memberOptions" :key="m.value" :label="m.label" :value="m.value" />
            </el-select>
          </el-form-item>
          <el-form-item label="活动名称">
            <el-select v-model="query.activityId" placeholder="请选择活动" filterable clearable :loading="activityLoading" @visible-change="loadActivities" style="width:180px">
              <el-option v-for="a in activityOptions" :key="a.id" :label="a.activityName" :value="a.id" />
            </el-select>
          </el-form-item>
          <template v-if="showAll">
            <el-form-item label="健康师">
              <el-select v-model="query.jksId" placeholder="健康师" clearable filterable style="width:150px">
                <el-option v-for="h in jksOptions" :key="h.id" :label="h.fullName" :value="h.id" />
              </el-select>
            </el-form-item>
            <el-form-item label="科技老师">
              <el-select v-model="query.kjblsId" placeholder="科技老师" clearable filterable style="width:150px">
                <el-option v-for="t in kjbOptions" :key="t.id" :label="t.fullName" :value="t.id" />
              </el-select>
            </el-form-item>
            <el-form-item label="付款方式">
              <el-select v-model="query.fkfs" placeholder="付款方式" clearable style="width:120px">
                <el-option v-for="p in payOptions" :key="p" :label="p" :value="p" />
              </el-select>
            </el-form-item>
            <el-form-item label="是否首开">
              <el-select v-model="query.sfskdd" placeholder="请选择" clearable style="width:100px">
                <el-option label="是" value="是" /><el-option label="否" value="否" />
              </el-select>
            </el-form-item>
            <el-form-item label="是否作废">
              <el-select v-model="query.isEffective" placeholder="请选择" clearable style="width:100px">
                <el-option label="正常" value="1" /><el-option label="作废" value="-1" />
              </el-select>
            </el-form-item>
          </template>
          <el-form-item>
            <el-button type="primary" @click="search">查询</el-button>
            <el-button @click="reset">重置</el-button>
            <el-button type="text" @click="showAll = !showAll">
              {{ showAll ? '收起' : '展开' }} <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
            </el-button>
          </el-form-item>
        </el-form>
      </div>

      <div class="dialog-content">
        <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f8fafc', color:'#64748b', fontWeight:600 }">
          <el-table-column type="expand" width="50">
            <template slot-scope="{ row }">
              <div class="expand-box" v-if="row.ItemDetails && row.ItemDetails.length">
                <div class="expand-title"><i class="el-icon-goods"></i> 品项明细</div>
                <el-table :data="row.ItemDetails" border size="mini" :header-cell-style="{ background:'#f0f4f8', color:'#64748b', fontWeight:500 }">
                  <el-table-column prop="pxmc" label="项目名称" width="180" />
                  <el-table-column label="项目价格" width="120" align="right">
                    <template slot-scope="s">¥{{ formatMoney(s.row.pxjg) }}</template>
                  </el-table-column>
                  <el-table-column prop="projectNumber" label="次数" width="80" align="right" />
                  <el-table-column label="总价" width="120" align="right">
                    <template slot-scope="s">¥{{ formatMoney(s.row.totalPrice) }}</template>
                  </el-table-column>
                  <el-table-column label="实付" width="120" align="right">
                    <template slot-scope="s"><span style="color:#67C23A;font-weight:600">¥{{ formatMoney(s.row.actualPrice) }}</span></template>
                  </el-table-column>
                  <el-table-column label="来源" width="100">
                    <template slot-scope="s"><el-tag size="mini" type="info">{{ s.row.sourceType || '-' }}</el-tag></template>
                  </el-table-column>
                  <el-table-column prop="remark" label="备注" show-overflow-tooltip />
                </el-table>
              </div>
              <div v-else class="expand-empty"><i class="el-icon-info"></i> 暂无品项明细</div>
            </template>
          </el-table-column>
          <el-table-column prop="kdhyc" label="开单会员" width="100" show-overflow-tooltip />
          <el-table-column prop="kdhysjh" label="会员手机号" width="120" />
          <el-table-column prop="activityName" label="活动名称" width="140" show-overflow-tooltip />
          <el-table-column label="开单日期" width="110">
            <template slot-scope="{ row }">{{ formatDate(row.kdrq) }}</template>
          </el-table-column>
          <el-table-column prop="zdyj" label="整单业绩" width="100" align="right" />
          <el-table-column prop="sfyj" label="实付业绩" width="100" align="right" />
          <el-table-column prop="deductAmount" label="储扣金额" width="100" align="right" />
          <el-table-column prop="qk" label="欠款" width="80" align="right" />
          <el-table-column label="付款方式" width="100">
            <template slot-scope="{ row }">{{ mapOption(row.fkfs, payOptions) }}</template>
          </el-table-column>
          <el-table-column label="是否首开" width="90">
            <template slot-scope="{ row }">{{ row.sfskdd || '-' }}</template>
          </el-table-column>
          <el-table-column label="是否作废" width="90">
            <template slot-scope="{ row }">{{ row.isEffective == '1' ? '正常' : '作废' }}</template>
          </el-table-column>
          <el-table-column label="操作人" width="120" show-overflow-tooltip>
            <template slot-scope="{ row }">{{ row.createUserName || '-' }}</template>
          </el-table-column>
        </el-table>
      </div>

      <div class="dialog-footer">
        <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size.sync="listQuery.pageSize" :current-page.sync="listQuery.currentPage" :page-sizes="[10, 20, 50, 100]" @size-change="initData" @current-change="initData" />
      </div>
    </div>
  </el-dialog>
</template>

<script>
const MOCK_BILLING = [
  { id: '1', djmdName: '千禧', kdhyc: '杨瑶', kdhysjh: '13516846588', activityName: '', kdrq: '2026-03-02 17:32:04', zdyj: '66.67', sfyj: '0.00', deductAmount: '66.67', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '郑巧玲', jksName: '黄泸娇', kjbName: '科技二部T区', ItemDetails: [{ pxmc: '胶原宝宝-单部位', pxjg: '66.67', projectNumber: '1', totalPrice: '66.67', actualPrice: '66.67', sourceType: '购买', remark: '' }] },
  { id: '2', djmdName: '红光', kdhyc: '张国菊', kdhysjh: '13893923967', activityName: '', kdrq: '2026-02-11 22:11:30', zdyj: '0.00', sfyj: '0.00', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '马丽亚', jksName: '马丽亚', kjbName: '', ItemDetails: [] },
  { id: '3', djmdName: '南湖', kdhyc: '林小芊', kdhysjh: '15902827650', activityName: '', kdrq: '2026-02-11 21:17:58', zdyj: '0.00', sfyj: '0.00', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '郝莉娜', jksName: '郝莉娜', kjbName: '', ItemDetails: [] },
  { id: '4', djmdName: '中和', kdhyc: '卢华瑜', kdhysjh: '13882365601', activityName: '', kdrq: '2026-02-11 19:30:27', zdyj: '1000.00', sfyj: '500.00', deductAmount: '0.00', qk: '500.00', fkfs: '微信', sfskdd: '否', isEffective: 1, createUserName: '贺慧', jksName: '李红梅,余姣姣', kjbName: '', ItemDetails: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '500.00', sourceType: '购买', remark: '' }] },
  { id: '5', djmdName: '沙河', kdhyc: '胡晗阳', kdhysjh: '13687006033', activityName: '', kdrq: '2026-02-11 19:19:51', zdyj: '1000.00', sfyj: '1000.00', deductAmount: '0.00', qk: '0.00', fkfs: '微信', sfskdd: '否', isEffective: 1, createUserName: '向郑瑶', jksName: '杨宜佳', kjbName: '', ItemDetails: [{ pxmc: '季卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '1000.00', sourceType: '购买', remark: '' }] },
  { id: '6', djmdName: '荣华北路', kdhyc: '何雪梅', kdhysjh: '13648005825', activityName: '', kdrq: '2026-02-11 18:51:41', zdyj: '19.90', sfyj: '19.90', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '是', isEffective: 1, createUserName: '谢娟', jksName: '荣华北路T区', kjbName: '', ItemDetails: [{ pxmc: '体验卡', pxjg: '19.90', projectNumber: '1', totalPrice: '19.90', actualPrice: '19.90', sourceType: '购买', remark: '' }] },
  { id: '7', djmdName: '南湖', kdhyc: '吴敏', kdhysjh: '13882088033', activityName: '', kdrq: '2026-02-11 18:19:13', zdyj: '66.60', sfyj: '0.00', deductAmount: '66.60', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '刘九招', jksName: '南湖T区', kjbName: '', ItemDetails: [{ pxmc: '胶原宝宝-单部位', pxjg: '66.60', projectNumber: '1', totalPrice: '66.60', actualPrice: '66.60', sourceType: '购买', remark: '' }] },
  { id: '8', djmdName: '大源', kdhyc: '李雪', kdhysjh: '15008224185', activityName: '', kdrq: '2026-02-11 18:18:01', zdyj: '1000.00', sfyj: '1000.00', deductAmount: '0.00', qk: '0.00', fkfs: '支付宝', sfskdd: '否', isEffective: 1, createUserName: '张红霞', jksName: '钟秦', kjbName: '', ItemDetails: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '1000.00', sourceType: '购买', remark: '' }] }
]
export default {
  name: 'BillingListDialog',
  props: { visible: { type: Boolean, default: false } },
  data() {
    return {
      mockData: MOCK_BILLING,
      showAll: false, loading: false, memberLoading: false, activityLoading: false,
      memberOptions: [], activityOptions: [], activityLoaded: false,
      jksOptions: [], kjbOptions: [],
      list: [], total: 0,
      query: { kdrq: undefined, kdhy: undefined, activityId: undefined, jksId: undefined, kjblsId: undefined, fkfs: undefined, sfskdd: undefined, isEffective: undefined },
      listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' },
      payOptions: ['现金', '微信', '支付宝', '银行卡', '合作', '直播收款', '合作方退']
    }
  },
  computed: {
    visibleProxy: { get() { return this.visible }, set(v) { this.$emit('update:visible', v) } }
  },
  watch: {
    visible(v) { if (v) { this.initData(); this.loadMembers(); this.loadStaff() } }
  },
  methods: {
    formatDate(ts) {
      if (!ts) return '-'
      if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10)
      const d = new Date(typeof ts === 'number' ? ts : Number(ts))
      if (isNaN(d.getTime())) return '-'
      return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
    },
    formatMoney(v) { return v != null ? Number(v).toFixed(2) : '0.00' },
    mapOption(val, opts) { return val || '-' },
    loadMembers() {
      const seen = new Map()
      this.mockData.forEach(r => { if (!seen.has(r.kdhysjh)) { seen.set(r.kdhysjh, true); this.memberOptions.push({ value: r.kdhysjh, label: `${r.kdhyc}(${r.kdhysjh})` }) } })
    },
    searchMember(kw) {
      if (!kw) return
      this.memberLoading = true
      setTimeout(() => {
        const lower = kw.toLowerCase()
        this.memberOptions = this.mockData
          .filter(r => r.kdhyc.toLowerCase().includes(lower) || r.kdhysjh.includes(kw))
          .reduce((acc, r) => { if (!acc.find(a => a.value === r.kdhysjh)) acc.push({ value: r.kdhysjh, label: `${r.kdhyc}(${r.kdhysjh})` }); return acc }, [])
        this.memberLoading = false
      }, 300)
    },
    loadActivities(v) {
      if (!v || this.activityLoaded) return
      this.activityLoading = true
      setTimeout(() => { this.activityLoaded = true; this.activityOptions = []; this.activityLoading = false }, 300)
    },
    loadStaff() {
      const jksSet = new Map(), kjbSet = new Map()
      this.mockData.forEach(r => {
        if (r.jksName) r.jksName.split(',').forEach(n => { const name = n.trim(); if (name && !jksSet.has(name)) { jksSet.set(name, true); this.jksOptions.push({ id: name, fullName: name }) } })
        if (r.kjbName) r.kjbName.split(',').forEach(n => { const name = n.trim(); if (name && !kjbSet.has(name)) { kjbSet.set(name, true); this.kjbOptions.push({ id: name, fullName: name }) } })
      })
    },
    initData() {
      this.loading = true
      setTimeout(() => {
        let filtered = [...this.mockData]
        if (this.query.kdrq && this.query.kdrq.length === 2) {
          const [s, e] = this.query.kdrq
          filtered = filtered.filter(r => { const t = new Date(r.kdrq).getTime(); return t >= s && t <= e + 86400000 })
        }
        if (this.query.kdhy) filtered = filtered.filter(r => r.kdhysjh === this.query.kdhy)
        if (this.query.jksId) filtered = filtered.filter(r => r.jksName && r.jksName.includes(this.query.jksId))
        if (this.query.kjblsId) filtered = filtered.filter(r => r.kjbName && r.kjbName.includes(this.query.kjblsId))
        if (this.query.fkfs) filtered = filtered.filter(r => r.fkfs === this.query.fkfs)
        if (this.query.sfskdd) filtered = filtered.filter(r => r.sfskdd === this.query.sfskdd)
        if (this.query.isEffective) filtered = filtered.filter(r => String(r.isEffective) === this.query.isEffective)
        this.total = filtered.length
        const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize
        this.list = filtered.slice(start, start + this.listQuery.pageSize)
        this.loading = false
      }, 300)
    },
    search() { this.listQuery.currentPage = 1; this.initData() },
    reset() { for (const k in this.query) this.query[k] = undefined; this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }; this.initData() }
  }
}
</script>

<style lang="scss" scoped>
::v-deep .billing-list-dialog { max-width: 1600px; margin-top: 3vh !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 .el-dialog__header { display: none; }
::v-deep .el-dialog__body { padding: 0; }
.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; }
.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 { font-size: 17px; font-weight: 600; color: #0f172a; }
.dialog-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; } }
.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; }
.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; }
.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); }
.expand-box { padding: 14px; background: #fafafa; border-radius: 8px; margin: 6px 0; .expand-title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 10px; i { color: #409EFF; } } }
.expand-empty { padding: 16px; text-align: center; color: #909399; font-size: 13px; i { margin-right: 4px; } }
::v-deep .billing-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } }
::v-deep .billing-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); }
::v-deep .billing-list-dialog .el-button--default { border-radius: 999px; }
::v-deep .billing-list-dialog .el-form-item { margin-bottom: 8px; }
::v-deep .billing-list-dialog .el-form-item__label { white-space: nowrap; }
::v-deep .billing-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; }
</style>