Samples.vue 10.8 KB
<template>
  <div class="samples-page">
    <div class="page-header">
      <h1>样本检测状态</h1>
      <div class="header-actions">
        <el-button @click="syncStatus" :loading="syncing">🔄 同步检测状态</el-button>
        <el-button type="primary" @click="showBatchDialog = true">批量录入</el-button>
      </div>
    </div>

    <div class="filter-bar">
      <el-input v-model="searchText" placeholder="搜索样本条码、患者姓名..." clearable style="width:280px" />
      <el-select v-model="platformFilter" clearable placeholder="检测平台" style="width:140px">
        <el-option label="Illumina" value="Illumina" />
        <el-option label="LIFE" value="LIFE" />
        <el-option label="Genolab" value="Genolab" />
      </el-select>
    </div>

    <el-table :data="filteredSamples" border row-key="id" style="width:100%">
      <el-table-column type="selection" width="45" />
      <el-table-column label="样本条码" width="155">
        <template #default="{ row }">
          <span class="barcode">{{ row.barcode }}</span>
        </template>
      </el-table-column>
      <el-table-column label="患者姓名" width="90">
        <template #default="{ row }">{{ getOrder(row._orderId)?.patientName || '—' }}</template>
      </el-table-column>
      <el-table-column label="亲缘关系" prop="relation" width="90" />
      <el-table-column label="检测类型" width="90">
        <template #default="{ row }">
          <el-tag size="small" effect="light">{{ getOrder(row._orderId)?.productType || '—' }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="检测平台" width="100">
        <template #default="{ row }">{{ row.detectionPlatform || '—' }}</template>
      </el-table-column>
      <el-table-column label="文库日期" width="110">
        <template #default="{ row }">{{ row.libraryDate || '—' }}</template>
      </el-table-column>
      <el-table-column label="Q30 (%)">
        <template #default="{ row }">
          <template v-if="row.q30">
            <el-progress :percentage="row.q30" :color="row.q30 >= 85 ? '#52c41a' : '#ff4d4f'" :stroke-width="8" :show-text="true" />
          </template>
          <span v-else style="color:#bbb">—</span>
        </template>
      </el-table-column>
      <el-table-column label="Dup (%)" width="80">
        <template #default="{ row }">
          <span :style="{ color: row.duplicate > 20 ? '#ff4d4f' : '#52c41a' }">{{ row.duplicate || '—' }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="220">
        <template #default="{ row }">
          <el-button size="small" text type="primary" @click="openDetDialog(row)">录入检测信息</el-button>
          <el-button size="small" text type="primary" @click="openDataDialog(row)">录入数据</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 录入检测信息弹窗 -->
    <el-dialog v-model="showDetDialog" title="快速录入检测信息" width="580px">
      <el-form :model="detForm" label-width="120px" size="small">
        <el-form-item label="检测地点">
          <el-select v-model="detForm.location" style="width:100%">
            <el-option label="北京实验室" value="北京" />
            <el-option label="上海实验室" value="上海" />
          </el-select>
        </el-form-item>
        <el-form-item label="检测平台">
          <el-radio-group v-model="detForm.platform">
            <el-radio label="Illumina">Illumina</el-radio>
            <el-radio label="LIFE">LIFE</el-radio>
            <el-radio label="Genolab">Genolab</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="文库构建日期"><el-date-picker v-model="detForm.libraryDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
        <el-form-item label="文库浓度(ng/µL)"><el-input-number v-model="detForm.libraryConc" :precision="2" style="width:160px" /></el-form-item>
        <el-form-item label="试剂盒批号"><el-input v-model="detForm.kitLot" /></el-form-item>
        <el-form-item label="测序日期"><el-date-picker v-model="detForm.seqDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
        <el-form-item label="测序仪序列号"><el-input v-model="detForm.seqSN" /></el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showDetDialog = false">取消</el-button>
        <el-button type="primary" @click="saveDetInfo">保存</el-button>
      </template>
    </el-dialog>

    <!-- 录入数据质量弹窗 -->
    <el-dialog v-model="showDataDialog" title="快速录入数据质量指标" width="500px">
      <el-form :model="dataForm" label-width="150px" size="small">
        <el-form-item label="Reads PF (M)"><el-input-number v-model="dataForm.readsM" :precision="1" style="width:160px" /></el-form-item>
        <el-form-item label="Cluster PF (%)"><el-input-number v-model="dataForm.clusterPf" :min="0" :max="100" :precision="1" style="width:160px" /></el-form-item>
        <el-form-item label="Q30 (%)"><el-input-number v-model="dataForm.q30" :min="0" :max="100" :precision="1" style="width:160px" /></el-form-item>
        <el-form-item label="Duplicate (%)"><el-input-number v-model="dataForm.duplicate" :min="0" :max="100" :precision="1" style="width:160px" /></el-form-item>
        <el-form-item label="Map Ratio (%)"><el-input-number v-model="dataForm.mapRatio" :min="0" :max="100" :precision="1" style="width:160px" /></el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showDataDialog = false">取消</el-button>
        <el-button type="primary" @click="saveDataInfo">保存</el-button>
      </template>
    </el-dialog>

    <!-- 批量录入弹窗 -->
    <el-dialog v-model="showBatchDialog" title="批量录入检测数据" width="700px">
      <div class="batch-table-tip">在下方表格中批量填写数据,留空表示不更新</div>
      <el-table :data="batchSamples" border size="small">
        <el-table-column label="条码" prop="barcode" width="150" />
        <el-table-column label="患者" width="80">
          <template #default="{ row }">{{ getOrder(row._orderId)?.patientName || '—' }}</template>
        </el-table-column>
        <el-table-column label="平台" width="120">
          <template #default="{ row }">
            <el-select v-model="row.detectionPlatform" size="small" style="width:100%">
              <el-option label="Illumina" value="Illumina" />
              <el-option label="LIFE" value="LIFE" />
              <el-option label="Genolab" value="Genolab" />
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="Q30" width="90">
          <template #default="{ row }">
            <el-input-number v-model="row.q30" :min="0" :max="100" :precision="1" size="small" style="width:80px" />
          </template>
        </el-table-column>
        <el-table-column label="Dup%" width="90">
          <template #default="{ row }">
            <el-input-number v-model="row.duplicate" :min="0" :max="100" :precision="1" size="small" style="width:80px" />
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="showBatchDialog = false">取消</el-button>
        <el-button type="primary" @click="saveBatch">批量保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

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

const pgxStore = usePGXStore()
const syncing = ref(false)

// 展平所有样本
const allSamples = computed(() => {
  const result: any[] = []
  pgxStore.orders.forEach(order => {
    order.samples.forEach(s => {
      result.push({ ...s, _orderId: order.id })
    })
  })
  return result
})

const searchText = ref('')
const platformFilter = ref('')

const filteredSamples = computed(() => {
  let list = allSamples.value
  if (searchText.value) {
    const kw = searchText.value.toLowerCase()
    list = list.filter((s: any) =>
      s.barcode.toLowerCase().includes(kw) ||
      (pgxStore.getOrderById(s._orderId)?.patientName || '').toLowerCase().includes(kw)
    )
  }
  if (platformFilter.value) {
    list = list.filter((s: any) => s.detectionPlatform === platformFilter.value)
  }
  return list
})

function getOrder(orderId: string) {
  return pgxStore.getOrderById(orderId)
}

async function syncStatus() {
  syncing.value = true
  await new Promise(r => setTimeout(r, 1200))
  syncing.value = false
  ElMessage.success('检测状态同步成功,已更新 3 条记录')
}

// 检测信息弹窗
const showDetDialog = ref(false)
const currentSample = ref<any>(null)
const detForm = ref({ location: '北京', platform: 'Illumina', libraryDate: '', libraryConc: 0, kitLot: '', seqDate: '', seqSN: '' })

function openDetDialog(row: any) {
  currentSample.value = row
  detForm.value = { location: '北京', platform: row.detectionPlatform || 'Illumina', libraryDate: row.libraryDate || '', libraryConc: 0, kitLot: '', seqDate: row.sequenceDate || '', seqSN: '' }
  showDetDialog.value = true
}

function saveDetInfo() {
  if (currentSample.value) {
    currentSample.value.detectionPlatform = detForm.value.platform
    currentSample.value.libraryDate = detForm.value.libraryDate
    currentSample.value.sequenceDate = detForm.value.seqDate
  }
  showDetDialog.value = false
  ElMessage.success('检测信息已录入')
}

// 数据质量弹窗
const showDataDialog = ref(false)
const dataForm = ref({ readsM: 0, clusterPf: 0, q30: 0, duplicate: 0, mapRatio: 0 })

function openDataDialog(row: any) {
  currentSample.value = row
  dataForm.value = { readsM: row.readsM || 0, clusterPf: 95, q30: row.q30 || 0, duplicate: row.duplicate || 0, mapRatio: row.mapRatio || 0 }
  showDataDialog.value = true
}

function saveDataInfo() {
  if (currentSample.value) {
    currentSample.value.q30 = dataForm.value.q30
    currentSample.value.duplicate = dataForm.value.duplicate
    currentSample.value.readsM = dataForm.value.readsM
    currentSample.value.mapRatio = dataForm.value.mapRatio
  }
  showDataDialog.value = false
  ElMessage.success('数据质量指标已录入')
}

// 批量录入
const showBatchDialog = ref(false)
const batchSamples = computed(() => allSamples.value.slice(0, 8).map(s => ({ ...s })))

function saveBatch() {
  showBatchDialog.value = false
  ElMessage.success('批量数据已保存')
}
</script>

<style scoped>
.samples-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; }
.header-actions { display: flex; gap: 10px; }
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; }
.barcode { font-family: monospace; font-size: 13px; color: #334155; }
.batch-table-tip { font-size: 12px; color: #888; margin-bottom: 10px; }
</style>