Tracking.vue 7.69 KB
<template>
  <div class="tracking-page">
    <div class="page-header">
      <h1>申请单状态追踪</h1>
    </div>

    <!-- 搜索 -->
    <div class="filter-bar">
      <el-input v-model="searchText" placeholder="搜索患者姓名、申请单编号..." clearable style="width:280px" />
      <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="接收日期" end-placeholder="" value-format="YYYY-MM-DD" style="width:280px" />
    </div>

    <el-tabs v-model="activeTab" class="status-tabs">
      <el-tab-pane name="received">
        <template #label>
          <span>接收审核 <el-badge :value="receivedList.length" :hidden="!receivedList.length" /></span>
        </template>
        <order-table :orders="receivedList" empty-text="暂无接收审核中的申请单" />
      </el-tab-pane>

      <el-tab-pane name="testing">
        <template #label>
          <span>正在检测 <el-badge :value="testingList.length" :hidden="!testingList.length" type="primary" /></span>
        </template>
        <order-table :orders="testingList" empty-text="暂无检测中的申请单" />
      </el-tab-pane>

      <el-tab-pane name="reported">
        <template #label>
          <span>已出报告 <el-badge :value="reportedList.length" :hidden="!reportedList.length" type="success" /></span>
        </template>
        <order-table :orders="reportedList" show-report empty-text="暂无已出报告的申请单" />
      </el-tab-pane>

      <el-tab-pane name="failed">
        <template #label>
          <span>检测异常 <el-badge :value="failedList.length" :hidden="!failedList.length" type="danger" /></span>
        </template>
        <!-- 异常列表增加异常原因列 -->
        <div v-if="failedList.length === 0" class="empty-tip">暂无检测异常申请单</div>
        <div v-else>
          <div v-for="order in failedList" :key="order.id" class="failed-card">
            <div class="failed-header">
              <span class="order-no">{{ order.orderNo }}</span>
              <el-tag type="danger" effect="light" size="small">检测异常</el-tag>
              <el-tag v-if="order.urgency" type="warning" size="small">加急</el-tag>
            </div>
            <div class="failed-body">
              <div class="failed-info">
                <span>患者:{{ order.patientName }}</span>
                <span>项目:{{ order.productType }}</span>
                <span>医院:{{ order.hospitalName }}</span>
                <span>日期:{{ order.submitDate }}</span>
              </div>
              <div v-if="order.failReason" class="fail-reason">
                <span class="reason-label">⚠ 异常原因:</span>{{ order.failReason }}
              </div>
              <div class="failed-actions">
                <el-button size="small" type="primary" @click="$router.push('/pgx/orders/' + order.id)">查看详情</el-button>
                <el-button size="small" type="warning" plain @click="$router.push('/pgx/orders/' + order.id + '?tab=supplement')">提交补充样本</el-button>
              </div>
            </div>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup lang="ts">
import { usePGXStore, type Order } from '@/stores/pgx'

const pgxStore = usePGXStore()
const router = useRouter()
const route = useRoute()

const searchText = ref('')
const dateRange = ref(null)
const activeTab = ref('received')

onMounted(() => {
  if (route.query.tab) activeTab.value = route.query.tab as string
})

function filterList(list: Order[]) {
  if (!searchText.value) return list
  const kw = searchText.value.toLowerCase()
  return list.filter(o =>
    o.patientName.toLowerCase().includes(kw) ||
    o.orderNo.toLowerCase().includes(kw)
  )
}

const receivedList = computed(() => filterList(pgxStore.receivedOrders))
const testingList = computed(() => filterList(pgxStore.testingOrders))
const reportedList = computed(() => filterList(pgxStore.reportedOrders))
const failedList = computed(() => filterList(pgxStore.failedOrders))

// 表格组件
const OrderTable = defineComponent({
  props: { orders: Array as () => Order[], emptyText: String, showReport: Boolean },
  setup(props) {
    const router2 = useRouter()
    return () => {
      if (!props.orders?.length) {
        return h('div', { class: 'empty-tip' }, props.emptyText)
      }
      return h('div', { class: 'order-table-wrap' }, [
        h('table', { class: 'tracking-table' }, [
          h('thead', {}, [
            h('tr', {}, [
              h('th', {}, '申请单编号'),
              h('th', {}, '患者姓名'),
              h('th', {}, '检测项目'),
              h('th', {}, '送检单位'),
              h('th', {}, '提交日期'),
              ...(props.showReport ? [h('th', {}, '报告')] : []),
              h('th', {}, '操作'),
            ])
          ]),
          h('tbody', {}, props.orders.map(o =>
            h('tr', { key: o.id, onClick: () => router2.push('/pgx/orders/' + o.id), style: { cursor: 'pointer' } }, [
              h('td', {}, [
                h('span', { style: { color:'#409eff', fontWeight:'500' } }, o.orderNo),
                o.urgency ? h('span', { style: { marginLeft:'6px', background:'#ff4d4f22', color:'#ff4d4f', borderRadius:'4px', padding:'1px 6px', fontSize:'11px' } }, '急') : null,
              ]),
              h('td', {}, o.patientName),
              h('td', {}, o.productType),
              h('td', {}, o.hospitalName),
              h('td', {}, o.submitDate),
              ...(props.showReport ? [h('td', {}, o.hasReport ? h('span', { style:{ color:'#52c41a' } }, '📄 有报告') : '—')] : []),
              h('td', { onClick: (e: Event) => e.stopPropagation() }, [
                h('button', { class: 'btn-link', onClick: () => router2.push('/pgx/orders/' + o.id) }, '查看详情'),
                props.showReport && o.hasReport ? h('button', { class: 'btn-link', style:{ marginLeft:'8px' }, onClick: () => {} }, '下载报告') : null,
              ]),
            ])
          ))
        ])
      ])
    }
  }
})
</script>

<style scoped>
.tracking-page { padding: 24px; }
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
.page-header h1 { font-size: 20px; font-weight: 600; margin: 0; color: #1a1f2e; }
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; }
.empty-tip { text-align: center; color: #aaa; padding: 60px; }

:deep(.tracking-table) {
  width: 100%;
  border-collapse: collapse;
  background: #fff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
:deep(.tracking-table th) {
  background: #f8faff;
  padding: 12px 16px;
  text-align: left;
  font-size: 13px;
  color: #666;
  font-weight: 500;
  border-bottom: 1px solid #e8ecf0;
}
:deep(.tracking-table td) {
  padding: 12px 16px;
  font-size: 13px;
  border-bottom: 1px solid #f5f5f5;
  color: #334155;
}
:deep(.tracking-table tr:hover td) { background: #f0f7ff; }
:deep(.btn-link) { background: none; border: none; color: #409eff; cursor: pointer; font-size: 13px; padding: 0; }
:deep(.btn-link:hover) { text-decoration: underline; }

.failed-card {
  background: #fff;
  border: 1px solid #ffe0e0;
  border-left: 4px solid #ff4d4f;
  border-radius: 8px;
  margin-bottom: 12px;
  padding: 14px 16px;
}
.failed-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.order-no { font-size: 14px; font-weight: 600; color: #1a1f2e; }
.failed-info { display: flex; gap: 20px; font-size: 13px; color: #666; margin-bottom: 8px; flex-wrap: wrap; }
.fail-reason { background: #fff1f0; border-radius: 6px; padding: 8px 12px; font-size: 13px; color: #cf1322; margin-bottom: 10px; }
.reason-label { font-weight: 600; }
.failed-actions { display: flex; gap: 10px; }
</style>