Dashboard.vue 13.9 KB
<template>
  <div class="dashboard">
    <div class="page-header">
      <h1>工作台</h1>
      <span class="date-text">{{ today }}</span>
    </div>

    <!-- Sampler 视图 -->
    <template v-if="pgxStore.currentRole === 'sampler'">
      <div class="stat-cards">
        <div class="stat-card">
          <div class="stat-icon" style="background:#e8f4fd">📋</div>
          <div class="stat-body">
            <div class="stat-num">{{ pgxStore.stats.total }}</div>
            <div class="stat-label">全部申请单</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#fff7e6">⏳</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#fa8c16">{{ pgxStore.stats.submitted }}</div>
            <div class="stat-label">待审核</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#e6f7ff">🧪</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#1890ff">{{ pgxStore.stats.testing }}</div>
            <div class="stat-label">检测中</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#f6ffed">✅</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#52c41a">{{ pgxStore.stats.reported }}</div>
            <div class="stat-label">已出报告</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#fff1f0">❌</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#ff4d4f">{{ pgxStore.stats.failed }}</div>
            <div class="stat-label">检测异常</div>
          </div>
        </div>
      </div>

      <div class="dashboard-grid">
        <div class="card">
          <div class="card-header">
            <span>最近申请单</span>
            <el-button size="small" text @click="$router.push('/pgx/orders')">查看全部 →</el-button>
          </div>
          <div class="order-list">
            <div v-for="order in recentOrders" :key="order.id" class="order-row" @click="$router.push('/pgx/orders/' + order.id)">
              <div class="order-info">
                <span class="order-no">{{ order.orderNo }}</span>
                <el-tag v-if="order.urgency" type="danger" size="small" effect="light" style="margin-left:6px">加急</el-tag>
                <div class="order-patient">{{ order.patientName }} · {{ order.productType }} · {{ order.hospitalName }}</div>
              </div>
              <div class="order-right">
                <status-badge :status="order.status" />
                <div class="order-date">{{ order.submitDate }}</div>
              </div>
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">快捷操作</div>
          <div class="quick-actions">
            <div class="quick-btn" @click="$router.push('/pgx/register')">
              <span class="q-icon">📝</span>
              <span>新建申请单</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/tracking')">
              <span class="q-icon">🔍</span>
              <span>查看状态</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/tracking?tab=reported')">
              <span class="q-icon">📄</span>
              <span>下载报告</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/post')">
              <span class="q-icon">📦</span>
              <span>邮寄查询</span>
            </div>
          </div>
        </div>
      </div>

      <!-- 待完善草稿 -->
      <div v-if="pgxStore.draftOrders.length > 0" class="card draft-alert">
        <div class="card-header">
          <span>📝 草稿箱({{ pgxStore.draftOrders.length }}份未提交)</span>
        </div>
        <div v-for="order in pgxStore.draftOrders" :key="order.id" class="draft-row">
          <span>{{ order.orderNo }} · {{ order.patientName || '未填写患者名' }} · {{ order.productType }}</span>
          <el-button size="small" type="primary" @click="$router.push('/pgx/orders/' + order.id)">继续填写</el-button>
        </div>
      </div>
    </template>

    <!-- CheckPointer 视图 -->
    <template v-else>
      <div class="stat-cards">
        <div class="stat-card">
          <div class="stat-icon" style="background:#fff7e6">📥</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#fa8c16">{{ pgxStore.stats.submitted }}</div>
            <div class="stat-label">待接收</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#e6f7ff">🧪</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#1890ff">{{ pgxStore.stats.testing }}</div>
            <div class="stat-label">检测中</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#f6ffed">✅</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#52c41a">{{ pgxStore.stats.reported }}</div>
            <div class="stat-label">已出报告</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#fff1f0">🔬</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#ff4d4f">{{ pgxStore.stats.failed }}</div>
            <div class="stat-label">检测异常</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#f9f0ff">⚡</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#722ed1">{{ bioinfoRunning }}</div>
            <div class="stat-label">生信分析中</div>
          </div>
        </div>
        <div class="stat-card">
          <div class="stat-icon" style="background:#e8f4fd">⚠️</div>
          <div class="stat-body">
            <div class="stat-num" style="color:#1890ff">{{ pgxStore.stats.limsError }}</div>
            <div class="stat-label">LIMS推送异常</div>
          </div>
        </div>
      </div>

      <div class="dashboard-grid">
        <div class="card">
          <div class="card-header">
            <span>待处理申请单</span>
            <el-button size="small" text @click="$router.push('/pgx/orders')">查看全部 →</el-button>
          </div>
          <div class="order-list">
            <div v-for="order in pendingOrders" :key="order.id" class="order-row" @click="$router.push('/pgx/orders/' + order.id)">
              <div class="order-info">
                <span class="order-no">{{ order.orderNo }}</span>
                <el-tag v-if="order.urgency" type="danger" size="small" effect="light" style="margin-left:6px">加急</el-tag>
                <div class="order-patient">{{ order.patientName }} · {{ order.productType }}</div>
              </div>
              <div class="order-right">
                <status-badge :status="order.status" />
                <lims-badge :status="order.limsStatus" />
              </div>
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">质控看板(实时)</div>
          <div class="qc-panel">
            <div v-for="m in qcMetrics" :key="m.label" class="qc-row">
              <div class="qc-label">{{ m.label }}</div>
              <el-progress
                :percentage="m.value"
                :color="m.color"
                :stroke-width="12"
                style="flex:1"
              />
              <div class="qc-value" :style="{ color: m.color }">{{ m.value }}%</div>
            </div>
          </div>

          <div class="card-header" style="margin-top:16px">快捷操作</div>
          <div class="quick-actions">
            <div class="quick-btn" @click="$router.push('/pgx/lims')">
              <span class="q-icon">✅</span><span>样本核对接收</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/samples')">
              <span class="q-icon">🧪</span><span>录入检测数据</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/bio')">
              <span class="q-icon">🔬</span><span>生信分析</span>
            </div>
            <div class="quick-btn" @click="$router.push('/pgx/report-gen')">
              <span class="q-icon">📄</span><span>报告审核签发</span>
            </div>
          </div>
        </div>
      </div>
    </template>
  </div>
</template>

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

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

const today = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' })

const recentOrders = computed(() => pgxStore.orders.slice(0, 5))
const pendingOrders = computed(() => pgxStore.orders.filter(o => ['submitted','received','testing'].includes(o.status)).slice(0, 6))
const bioinfoRunning = computed(() => pgxStore.bioinfoRuns.filter(r => r.status === 'running').length)

const qcMetrics = ref([
  { label: 'Q30 (%)', value: 91, color: '#52c41a' },
  { label: 'Cluster PF (%)', value: 95, color: '#1890ff' },
  { label: 'Duplicate (%)', value: 13, color: '#fa8c16' },
  { label: 'Map Ratio (%)', value: 98, color: '#52c41a' },
])

// 模拟实时波动
let timer: any
onMounted(() => {
  timer = setInterval(() => {
    qcMetrics.value[0].value = +(88 + Math.random() * 6).toFixed(1)
    qcMetrics.value[1].value = +(92 + Math.random() * 5).toFixed(1)
    qcMetrics.value[2].value = +(10 + Math.random() * 8).toFixed(1)
    qcMetrics.value[3].value = +(96 + Math.random() * 3).toFixed(1)
  }, 3000)
})
onUnmounted(() => clearInterval(timer))

// 状态徽章子组件
const StatusBadge = defineComponent({
  props: { status: String },
  setup(props) {
    const map: Record<string, { label: string; type: string }> = {
      draft: { label: '草稿', type: 'info' },
      submitted: { label: '已提交', type: 'warning' },
      received: { label: '接收审核', type: '' },
      testing: { label: '检测中', type: 'primary' },
      reported: { label: '已出报告', type: 'success' },
      failed: { label: '检测异常', type: 'danger' },
    }
    return () => {
      const m = map[props.status || 'draft']
      return h(resolveComponent('el-tag'), { type: m.type, size: 'small', effect: 'light' }, () => m.label)
    }
  }
})

const LimsBadge = defineComponent({
  props: { status: String },
  setup(props) {
    const map: Record<string, { icon: string; color: string; label: string }> = {
      pending: { icon: '⏳', color: '#aaa', label: 'LIMS待推送' },
      pushed: { icon: '✓', color: '#52c41a', label: 'LIMS已推送' },
      failed: { icon: '⚠', color: '#ff4d4f', label: 'LIMS推送异常' },
    }
    return () => {
      const m = map[props.status || 'pending']
      return h('span', { style: { color: m.color, fontSize: '12px' } }, m.icon + ' ' + m.label)
    }
  }
})
</script>

<style scoped>
.dashboard { padding: 24px; }

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}
.page-header h1 { font-size: 20px; font-weight: 600; margin: 0; color: #1a1f2e; }
.date-text { color: #8896aa; font-size: 14px; }

.stat-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.stat-card {
  background: #fff;
  border-radius: 10px;
  padding: 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
  cursor: pointer;
  transition: box-shadow 0.2s;
}
.stat-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.12); }
.stat-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.stat-num { font-size: 26px; font-weight: 700; line-height: 1; }
.stat-label { font-size: 13px; color: #8896aa; margin-top: 4px; }

.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-bottom: 16px;
}

.card {
  background: #fff;
  border-radius: 10px;
  padding: 16px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 600;
  color: #1a1f2e;
  margin-bottom: 12px;
  font-size: 14px;
}

.order-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid #f0f2f5;
  cursor: pointer;
  transition: background 0.15s;
  border-radius: 4px;
  padding-left: 4px;
}
.order-row:hover { background: #f8faff; }
.order-row:last-child { border-bottom: none; }
.order-no { font-size: 13px; font-weight: 500; color: #1890ff; }
.order-patient { font-size: 12px; color: #8896aa; margin-top: 3px; }
.order-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.order-date { font-size: 11px; color: #bbb; }

.quick-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.quick-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
  border: 1px solid #e8ecf0;
  border-radius: 8px;
  cursor: pointer;
  font-size: 13px;
  color: #334155;
  transition: all 0.15s;
}
.quick-btn:hover { background: #f0f7ff; border-color: #409eff; color: #409eff; }
.q-icon { font-size: 18px; }

.draft-alert { border-left: 4px solid #fa8c16; }
.draft-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 0;
  font-size: 13px;
  color: #666;
  border-bottom: 1px solid #f5f5f5;
}
.draft-row:last-child { border-bottom: none; }

.qc-panel { display: flex; flex-direction: column; gap: 10px; }
.qc-row { display: flex; align-items: center; gap: 12px; }
.qc-label { width: 100px; font-size: 12px; color: #666; flex-shrink: 0; }
.qc-value { width: 50px; text-align: right; font-size: 13px; font-weight: 600; }
</style>