feat(lab-sample): 实验室样本处理三表台账与部门字段 ACL
按造血/单个核/间充质聚合展示,部门员工只改本部门、主管可改全部,并支持筛选与 Excel 导出;同步纠正采集接受人串权与严格菜单守卫。
This commit is contained in:
1 parent
58b252ce6b
commit
5fe1193a2c
22 files changed
+2136
-5
No files matched your search
@@ -0,0 +1,329 @@
|
||||
<!-- 实验室样本处理数据 — 共享列表页 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">{{ pageTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="样本编码/姓名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
共 <span class="sel-num">{{ pagination.total }}</span> 条
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" :loading="exporting" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
size="small"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
v-for="col in visibleColumns"
|
||||
:key="col.key"
|
||||
:prop="col.key"
|
||||
:label="col.label"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="260"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="(acl.editableDepts || []).length"
|
||||
class="btn-ghost"
|
||||
link
|
||||
type="primary"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<span v-else class="text-muted">只读</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="loadData"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="editVisible"
|
||||
class="dialog-form-layout"
|
||||
:title="`编辑 — ${editRow?.sampleCode || ''}`"
|
||||
width="860px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form label-position="top" class="form-grid-2" v-loading="saving">
|
||||
<template v-for="group in editGroups" :key="group.dept">
|
||||
<div class="form-col-span-2 dept-title">{{ group.label }}</div>
|
||||
<el-form-item v-for="f in group.fields" :key="f.key" :label="f.label">
|
||||
<el-input
|
||||
v-model="editForm[f.key]"
|
||||
:disabled="!canEditDept(f.dept)"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="editVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="saving" @click="submitEdit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Check, Close, Download, Edit, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
fetchLabSampleList,
|
||||
updateLabSampleRow,
|
||||
type LabSampleAcl,
|
||||
type LabSampleDataset,
|
||||
type LabSampleFieldMeta
|
||||
} from '@/api/lab-sample-processing'
|
||||
|
||||
const props = defineProps<{
|
||||
dataset: LabSampleDataset
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const pageTitle = computed(() => props.title)
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const exporting = ref(false)
|
||||
const tableData = ref<Record<string, any>[]>([])
|
||||
const fields = ref<LabSampleFieldMeta[]>([])
|
||||
const acl = ref<LabSampleAcl>({})
|
||||
const searchForm = reactive({ keyword: '' })
|
||||
const dateRange = ref<string[] | null>(null)
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editRow = ref<Record<string, any> | null>(null)
|
||||
const editForm = reactive<Record<string, any>>({})
|
||||
|
||||
const deptLabel: Record<string, string> = {
|
||||
market: '市场字段',
|
||||
production: '生产字段',
|
||||
qc: '质检字段'
|
||||
}
|
||||
|
||||
const visibleColumns = computed(() => fields.value)
|
||||
|
||||
const editGroups = computed(() => {
|
||||
const groups = ['market', 'production', 'qc'].map((dept) => ({
|
||||
dept,
|
||||
label: deptLabel[dept] || dept,
|
||||
fields: fields.value.filter((f) => f.dept === dept)
|
||||
}))
|
||||
return groups.filter((g) => g.fields.length > 0)
|
||||
})
|
||||
|
||||
function canEditDept(dept: string) {
|
||||
const list = acl.value.editableDepts || []
|
||||
return list.includes(dept)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchLabSampleList(props.dataset, {
|
||||
keyword: searchForm.keyword || undefined,
|
||||
dateFrom: dateRange.value?.[0],
|
||||
dateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
tableData.value = res?.list || []
|
||||
pagination.total = res?.total || 0
|
||||
fields.value = res?.fields || []
|
||||
acl.value = res?.acl || {}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
void loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
dateRange.value = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function openEdit(row: Record<string, any>) {
|
||||
editRow.value = row
|
||||
Object.keys(editForm).forEach((k) => delete editForm[k])
|
||||
for (const f of fields.value) {
|
||||
editForm[f.key] = row[f.key] ?? ''
|
||||
}
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editRow.value?.sampleCode) return
|
||||
const editableKeys = fields.value
|
||||
.filter((f) => canEditDept(f.dept))
|
||||
.map((f) => f.key)
|
||||
if (!editableKeys.length) {
|
||||
ElMessage.warning('当前账号无可编辑字段')
|
||||
return
|
||||
}
|
||||
const payload: Record<string, any> = {}
|
||||
for (const key of editableKeys) {
|
||||
payload[key] = editForm[key]
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await updateLabSampleRow(props.dataset, editRow.value.sampleCode, payload)
|
||||
ElMessage.success('保存成功')
|
||||
editVisible.value = false
|
||||
await loadData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true
|
||||
try {
|
||||
const res = await fetchLabSampleList(props.dataset, {
|
||||
keyword: searchForm.keyword || undefined,
|
||||
dateFrom: dateRange.value?.[0],
|
||||
dateTo: dateRange.value?.[1],
|
||||
page: 1,
|
||||
pageSize: 5000
|
||||
})
|
||||
const cols = res?.fields?.length ? res.fields : fields.value
|
||||
const rows = res?.list || []
|
||||
const ExcelJS = (await import('exceljs')).default
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const sheet = workbook.addWorksheet(props.title || props.dataset)
|
||||
sheet.addRow(cols.map((c) => c.label))
|
||||
for (const row of rows) {
|
||||
sheet.addRow(cols.map((c) => row[c.key] ?? ''))
|
||||
}
|
||||
const header = sheet.getRow(1)
|
||||
header.font = { bold: true }
|
||||
const buffer = await workbook.xlsx.writeBuffer()
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `${props.title || props.dataset}.xlsx`
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
ElMessage.success(`已导出 ${rows.length} 条`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dept-title {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary);
|
||||
margin: 4px 0 8px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<LabSampleProcessingPage dataset="hsc" title="造血干细胞数据" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LabSampleProcessingPage from '../LabSampleProcessingPage.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<LabSampleProcessingPage dataset="msc" title="脐带间充质干细胞数据" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LabSampleProcessingPage from '../LabSampleProcessingPage.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<LabSampleProcessingPage dataset="ubmc" title="单个核数据" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LabSampleProcessingPage from '../LabSampleProcessingPage.vue'
|
||||
</script>
|
||||
Reference in new issue
Block a user