Samples.vue
10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
<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>