提交
This commit is contained in:
1 parent
ee73f69759
commit
74f751d0d1
70 files changed
+9340
-6930
No files matched your search
@@ -404,6 +404,7 @@
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
show-storage-cert
|
||||
show-fullmoon-insurance
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<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-input v-model="searchForm.keyword" placeholder="协议号/姓名/身份证号/电话" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="协议状态">
|
||||
<el-select
|
||||
@@ -290,6 +290,7 @@
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
show-storage-cert
|
||||
show-fullmoon-insurance
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="1200px"
|
||||
width="1380px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
|
||||
@@ -366,6 +366,10 @@
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">体积(ml)</div>
|
||||
<div class="data-value">{{ formatMetric(row.sampleReception.volumeMl) }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">净重(g)</div>
|
||||
<div class="data-value">{{ formatMetric(row.sampleReception.netWeightG) }}</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<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-input v-model="searchForm.keyword" placeholder="协议号/姓名/身份证号/电话" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="协议状态">
|
||||
<el-select
|
||||
@@ -255,6 +255,8 @@
|
||||
:protocol-id="viewProtocolId"
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
show-storage-cert
|
||||
show-fullmoon-insurance
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -245,13 +245,14 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看弹窗(与采集协议单录入一致) -->
|
||||
<!-- 查看弹窗(与满月待办管理等一致:审核+储存证+保险) -->
|
||||
<ProtocolViewDialog
|
||||
v-model="viewVisible"
|
||||
:protocol-id="viewProtocolId"
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
show-storage-cert
|
||||
show-fullmoon-insurance
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -327,7 +328,7 @@
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 查看弹窗(与采集协议单录入一致)
|
||||
// 查看弹窗(与满月待办管理等一致)
|
||||
// ---------------------------------------------------------------------------
|
||||
const viewVisible = ref(false)
|
||||
const viewProtocolId = ref<string | null>(null)
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="840px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="项目编码" prop="itemCode">
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
<el-dialog
|
||||
v-model="resultVisible"
|
||||
title="填写检测结果"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
:width="resultDialogWidth"
|
||||
class="dialog-form-layout qc-task-result-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@@ -160,7 +160,7 @@
|
||||
>
|
||||
该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<div class="form-grid-3">
|
||||
<el-form-item
|
||||
v-for="(param, idx) in resultParams"
|
||||
:key="param.paramId || param.paramCode || idx"
|
||||
@@ -178,42 +178,85 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-section material-usage-section">
|
||||
<div class="section-title">物料使用(选填)</div>
|
||||
<div v-for="(mu, idx) in materialUsages" :key="idx" class="material-usage-row">
|
||||
<el-form-item label="物料" class="mu-field-material">
|
||||
<el-select
|
||||
v-model="mu.materialId"
|
||||
filterable
|
||||
placeholder="选择物料"
|
||||
style="width: 100%"
|
||||
@change="(v: string) => onMaterialSelect(mu, v)"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in materialOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.label"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="使用数量" class="mu-field-qty">
|
||||
<el-input-number
|
||||
v-model="mu.qty"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="数量"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button class="btn-ghost mu-remove-btn" @click="materialUsages.splice(idx, 1)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
<div v-if="resultTaskType !== 'dsfjc'" class="form-section material-usage-section">
|
||||
<div class="section-title material-usage-head">
|
||||
<div class="section-title-left"><h3>物料使用(选填)</h3></div>
|
||||
<el-button class="btn-main" type="primary" size="small" @click="addMaterialRow">
|
||||
<el-icon><Plus /></el-icon>添加物料
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button class="btn-ghost" size="small" @click="materialUsages.push({ materialId: '', materialName: '', qty: undefined })">
|
||||
<el-icon><Plus /></el-icon>添加物料
|
||||
</el-button>
|
||||
<el-table
|
||||
v-if="materialUsages.length"
|
||||
:data="materialUsages"
|
||||
:row-key="(row) => row._rowKey"
|
||||
stripe
|
||||
class="list-table inner-table material-usage-table"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column label="物料名称" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-model="row.materialId"
|
||||
filterable
|
||||
placeholder="搜索并选择物料"
|
||||
class="inner-cell-input"
|
||||
@change="(v) => onMaterialSelect($index, v)"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in materialOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.label"
|
||||
:value="opt.id"
|
||||
:disabled="isMaterialOptionDisabled(opt.id, $index)"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.specModel || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用量" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.qty"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
placeholder="用量"
|
||||
class="inner-cell-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="选填" clearable class="inner-cell-input" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="96" align="center" header-align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" size="small" @click="removeMaterialRow($index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty
|
||||
v-else
|
||||
description="暂无物料记录,可点击上方按钮添加"
|
||||
:image-size="56"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-section result-conclusion-section">
|
||||
<div class="section-title">检测结论</div>
|
||||
@@ -267,6 +310,49 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<div
|
||||
v-if="viewDetail?.taskType !== 'dsfjc'"
|
||||
class="view-section qc-task-view-material-usages"
|
||||
>
|
||||
<h3>物料使用</h3>
|
||||
<el-table
|
||||
v-if="viewMaterialUsages.length"
|
||||
:data="viewMaterialUsages"
|
||||
stripe
|
||||
class="list-table inner-table qc-task-view-material-table"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="materialName"
|
||||
label="物料名称"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.specModel || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用量" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.remark || '—' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="view-material-empty">暂无物料使用记录</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="viewDetail?.attachmentFiles?.length"
|
||||
class="view-section qc-task-view-attachments"
|
||||
@@ -287,7 +373,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen } from '@element-plus/icons-vue'
|
||||
import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen, Plus } from '@element-plus/icons-vue'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
@@ -303,7 +389,13 @@
|
||||
type QcTask,
|
||||
type QcTaskDetail
|
||||
} from '@/api/qc-task'
|
||||
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
|
||||
import {
|
||||
fetchQcTaskResults,
|
||||
fetchQcTaskMaterialUsages,
|
||||
fetchCompleteTaskWithResult,
|
||||
type QcTaskResultItem,
|
||||
type QcTaskMaterialUsageItem
|
||||
} from '@/api/qc-task-result'
|
||||
import { fetchQcMaterialOptions, type QcMaterialOption } from '@/api/qc-material'
|
||||
import {
|
||||
taskTypeLabel,
|
||||
@@ -333,8 +425,28 @@
|
||||
const resultRemark = ref('')
|
||||
const resultConclusion = ref('')
|
||||
const resultAttachments = ref<FinanceAttachmentItem[]>([])
|
||||
type MaterialUsageRow = {
|
||||
_rowKey: string
|
||||
materialId: string
|
||||
materialName: string
|
||||
specModel: string
|
||||
unit: string
|
||||
qty: number | undefined
|
||||
remark: string
|
||||
}
|
||||
|
||||
let materialUsageRowSeq = 0
|
||||
function nextMaterialRowKey() {
|
||||
materialUsageRowSeq += 1
|
||||
return `mu_${Date.now()}_${materialUsageRowSeq}`
|
||||
}
|
||||
|
||||
const materialOptions = ref<QcMaterialOption[]>([])
|
||||
const materialUsages = ref<Array<{ materialId: string; materialName: string; qty: number | undefined }>>([])
|
||||
const materialUsages = ref<MaterialUsageRow[]>([])
|
||||
|
||||
const resultDialogWidth = computed(() =>
|
||||
resultTaskType.value === 'sysz' ? '970px' : '720px'
|
||||
)
|
||||
|
||||
function canFillResult(row: QcTask) {
|
||||
if (row.taskType === 'sysz') {
|
||||
@@ -358,13 +470,111 @@
|
||||
resultTaskType.value === 'dsfjc' ? 'qc-third-party' : 'qc-lab'
|
||||
)
|
||||
|
||||
function onMaterialSelect(mu: { materialId: string; materialName: string; qty: number | undefined }, id: string) {
|
||||
function clearMaterialRow(row: MaterialUsageRow) {
|
||||
row.materialId = ''
|
||||
row.materialName = ''
|
||||
row.specModel = ''
|
||||
row.unit = ''
|
||||
}
|
||||
|
||||
function isMaterialOptionDisabled(materialId: string, currentIndex: number) {
|
||||
if (!materialId) return false
|
||||
return materialUsages.value.some((r, i) => i !== currentIndex && r.materialId === materialId)
|
||||
}
|
||||
|
||||
function isDuplicateMaterial(materialId: string, excludeIndex: number) {
|
||||
if (!materialId) return false
|
||||
return materialUsages.value.some((r, i) => i !== excludeIndex && r.materialId === materialId)
|
||||
}
|
||||
|
||||
function parseMaterialQty(qty: number | string | undefined): number | null {
|
||||
if (qty === undefined || qty === null || qty === '') return null
|
||||
const n = typeof qty === 'number' ? qty : Number(String(qty).trim())
|
||||
if (!Number.isFinite(n) || n <= 0) return null
|
||||
return n
|
||||
}
|
||||
|
||||
function onMaterialSelect(index: number, id: string) {
|
||||
const row = materialUsages.value[index]
|
||||
if (!row) return
|
||||
if (!id) {
|
||||
if (parseMaterialQty(row.qty) == null) {
|
||||
removeMaterialRow(index)
|
||||
} else {
|
||||
clearMaterialRow(row)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isDuplicateMaterial(id, index)) {
|
||||
ElMessage.warning('该物料已在列表中,请勿重复添加')
|
||||
clearMaterialRow(row)
|
||||
return
|
||||
}
|
||||
const opt = materialOptions.value.find((o) => o.id === id)
|
||||
mu.materialName = opt ? opt.materialName : ''
|
||||
row.materialId = id
|
||||
row.materialName = opt?.materialName || ''
|
||||
row.specModel = opt?.specModel || ''
|
||||
row.unit = opt?.unit || ''
|
||||
}
|
||||
|
||||
function addMaterialRow() {
|
||||
materialUsages.value.push({
|
||||
_rowKey: nextMaterialRowKey(),
|
||||
materialId: '',
|
||||
materialName: '',
|
||||
specModel: '',
|
||||
unit: '',
|
||||
qty: undefined,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
function removeMaterialRow(index: number) {
|
||||
materialUsages.value = materialUsages.value.filter((_, i) => i !== index)
|
||||
}
|
||||
|
||||
/** 合并同物料多条变动记录(历史重复扣减时),并生成稳定行 key */
|
||||
function materialRowsFromApi(items: QcTaskMaterialUsageItem[]): MaterialUsageRow[] {
|
||||
const merged = new Map<string, MaterialUsageRow>()
|
||||
for (const u of items || []) {
|
||||
const mid = (u.materialId || '').trim()
|
||||
if (!mid) continue
|
||||
const qtyRaw = Math.abs(Number(u.changeQty) || 0)
|
||||
const prev = merged.get(mid)
|
||||
if (prev) {
|
||||
prev.qty = (Number(prev.qty) || 0) + qtyRaw
|
||||
} else {
|
||||
merged.set(mid, {
|
||||
_rowKey: nextMaterialRowKey(),
|
||||
materialId: mid,
|
||||
materialName: u.materialName || '',
|
||||
specModel: '',
|
||||
unit: '',
|
||||
qty: qtyRaw > 0 ? qtyRaw : undefined,
|
||||
remark: u.remark || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
|
||||
function enrichMaterialUsageRows(
|
||||
rows: MaterialUsageRow[],
|
||||
options: QcMaterialOption[] = materialOptions.value
|
||||
) {
|
||||
for (const row of rows) {
|
||||
const opt = options.find((o) => o.id === row.materialId)
|
||||
if (opt) {
|
||||
row.materialName = opt.materialName || row.materialName
|
||||
row.specModel = opt.specModel || ''
|
||||
row.unit = opt.unit || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewDetail = ref<QcTaskDetail | null>(null)
|
||||
const viewMaterialUsages = ref<MaterialUsageRow[]>([])
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function apiFilesToFinanceItems(
|
||||
@@ -603,6 +813,20 @@
|
||||
} catch {
|
||||
;(detail as any).results = []
|
||||
}
|
||||
viewMaterialUsages.value = []
|
||||
if (row.taskType !== 'dsfjc') {
|
||||
try {
|
||||
const [savedUsages, opts] = await Promise.all([
|
||||
fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[]),
|
||||
fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
|
||||
])
|
||||
const rows = materialRowsFromApi(savedUsages || [])
|
||||
enrichMaterialUsageRows(rows, opts || [])
|
||||
viewMaterialUsages.value = rows
|
||||
} catch {
|
||||
viewMaterialUsages.value = []
|
||||
}
|
||||
}
|
||||
viewDetail.value = detail
|
||||
viewVisible.value = true
|
||||
}
|
||||
@@ -623,6 +847,7 @@
|
||||
resultConclusion.value = ''
|
||||
resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
|
||||
materialUsages.value = []
|
||||
materialOptions.value = []
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const detail = await fetchQcTaskDetail(row.id)
|
||||
@@ -632,16 +857,21 @@
|
||||
resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
|
||||
const results = await fetchQcTaskResults(row.id)
|
||||
resultParams.value = normalizeResultParams(results || [])
|
||||
if (row.taskType !== 'dsfjc') {
|
||||
const [opts, savedUsages] = await Promise.all([
|
||||
fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[]),
|
||||
fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[])
|
||||
])
|
||||
materialOptions.value = opts || []
|
||||
materialUsages.value = materialRowsFromApi(savedUsages || [])
|
||||
enrichMaterialUsageRows(materialUsages.value)
|
||||
}
|
||||
} catch {
|
||||
resultParams.value = []
|
||||
materialUsages.value = []
|
||||
} finally {
|
||||
resultLoading.value = false
|
||||
}
|
||||
try {
|
||||
materialOptions.value = await fetchQcMaterialOptions()
|
||||
} catch {
|
||||
materialOptions.value = []
|
||||
}
|
||||
resultVisible.value = true
|
||||
}
|
||||
|
||||
@@ -658,17 +888,33 @@
|
||||
ElMessage.warning('请上传第三方检测材料')
|
||||
return
|
||||
}
|
||||
if (resultTaskType.value !== 'dsfjc') {
|
||||
materialUsages.value = materialUsages.value.filter((mu) => !!mu.materialId)
|
||||
const seenMaterialIds = new Set<string>()
|
||||
for (const mu of materialUsages.value) {
|
||||
if (!mu.materialId) continue
|
||||
if (parseMaterialQty(mu.qty) == null) {
|
||||
ElMessage.warning('请为已选物料填写有效用量')
|
||||
return
|
||||
}
|
||||
if (seenMaterialIds.has(mu.materialId)) {
|
||||
ElMessage.warning('物料列表中存在重复物料,请检查')
|
||||
return
|
||||
}
|
||||
seenMaterialIds.add(mu.materialId)
|
||||
}
|
||||
}
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const validMaterialUsages = materialUsages.value.filter(
|
||||
(mu) => mu.materialId && mu.qty && mu.qty > 0
|
||||
)
|
||||
const validMaterialUsages = materialUsages.value
|
||||
.map((mu) => ({ ...mu, qtyNum: parseMaterialQty(mu.qty) }))
|
||||
.filter((mu) => mu.materialId && mu.qtyNum != null)
|
||||
const data: {
|
||||
conclusion: string
|
||||
results: QcTaskResultItem[]
|
||||
remark?: string
|
||||
thirdPartyAttachments?: string
|
||||
materialUsages?: Array<{ materialId: string; qty: number }>
|
||||
materialUsages?: Array<{ materialId: string; qty: number; remark?: string }>
|
||||
} = {
|
||||
conclusion: resultConclusion.value,
|
||||
remark: resultRemark.value,
|
||||
@@ -680,10 +926,11 @@
|
||||
}))
|
||||
}
|
||||
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
|
||||
if (validMaterialUsages.length > 0) {
|
||||
if (resultTaskType.value !== 'dsfjc') {
|
||||
data.materialUsages = validMaterialUsages.map((mu) => ({
|
||||
materialId: mu.materialId,
|
||||
qty: mu.qty!,
|
||||
qty: mu.qtyNum!,
|
||||
remark: mu.remark?.trim() || undefined
|
||||
}))
|
||||
}
|
||||
await fetchCompleteTaskWithResult(resultTaskId.value, data)
|
||||
@@ -719,24 +966,50 @@
|
||||
.material-usage-section {
|
||||
margin-top: 12px;
|
||||
|
||||
.material-usage-row {
|
||||
.material-usage-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.mu-field-material {
|
||||
flex: 3;
|
||||
.section-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: #10b981;
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mu-field-qty {
|
||||
flex: 1.2;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.mu-remove-btn {
|
||||
margin-top: 28px;
|
||||
padding: 8px;
|
||||
}
|
||||
.material-usage-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner-cell-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.inner-cell-input.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,6 +1052,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.qc-task-view-material-usages,
|
||||
.qc-task-view-attachments {
|
||||
padding: 18px 22px;
|
||||
border-bottom: none;
|
||||
@@ -794,6 +1068,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.qc-task-view-material-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-material-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.view-reception-attachments {
|
||||
margin-top: 4px;
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="protocolSampleId"
|
||||
:row-class-name="sampleReceptionRowClassName"
|
||||
@row-dblclick="handleRowDblClick"
|
||||
@selection-change="handlePendingSelectionChange"
|
||||
>
|
||||
@@ -679,6 +680,35 @@
|
||||
return Number.isFinite(n) ? String(n) : '-'
|
||||
}
|
||||
|
||||
function parseEtaLocalDate(eta: unknown): Date | null {
|
||||
const s = String(eta || '').trim()
|
||||
if (!s) return null
|
||||
const normalized = s.replace('T', ' ').slice(0, 19)
|
||||
const m = normalized.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||
if (!m) return null
|
||||
const y = Number(m[1])
|
||||
const mo = Number(m[2]) - 1
|
||||
const d = Number(m[3])
|
||||
const dt = new Date(y, mo, d)
|
||||
if (dt.getFullYear() !== y || dt.getMonth() !== mo || dt.getDate() !== d) return null
|
||||
return dt
|
||||
}
|
||||
|
||||
function startOfLocalDay(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
}
|
||||
|
||||
function isEtaTodayOrEarlier(etaTime: unknown) {
|
||||
const etaDay = parseEtaLocalDate(etaTime)
|
||||
if (!etaDay) return false
|
||||
return etaDay.getTime() <= startOfLocalDay(new Date()).getTime()
|
||||
}
|
||||
|
||||
function sampleReceptionRowClassName({ row }: { row: Record<string, unknown> }) {
|
||||
if (activeTab.value !== 'pending') return ''
|
||||
return isEtaTodayOrEarlier(row.etaTime) ? 'row-eta-overdue' : ''
|
||||
}
|
||||
|
||||
function apiFilesToFinanceItems(
|
||||
files?: Array<{ id?: string; fileUrl?: string; fileName?: string; fileType?: string }>
|
||||
): FinanceAttachmentItem[] {
|
||||
@@ -1236,6 +1266,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list-table :deep(tr.row-eta-overdue > td.el-table__cell) {
|
||||
background-color: #fee2e2 !important;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.list-table :deep(tr.row-eta-overdue:hover > td.el-table__cell) {
|
||||
background-color: #fecaca !important;
|
||||
}
|
||||
|
||||
.sample-reception-dialog .check-list-view,
|
||||
.batch-receive-dialog .check-list-view {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -126,7 +126,9 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<div class="section-title">开票信息</div>
|
||||
<div class="section-title section-title--static">
|
||||
<div class="section-title-left"><h3>开票信息</h3></div>
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="发票抬头">
|
||||
<el-input v-model="companyForm.invoiceTitle" placeholder="与营业执照一致" />
|
||||
@@ -167,8 +169,8 @@
|
||||
<el-dialog
|
||||
v-model="itemDialogVisible"
|
||||
:title="itemDialogTitle"
|
||||
width="880px"
|
||||
class="dialog-form-layout"
|
||||
width="980px"
|
||||
class="dialog-form-layout third-party-item-config-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@@ -194,7 +196,7 @@
|
||||
v-loading="itemLoading"
|
||||
stripe
|
||||
row-key="id"
|
||||
max-height="360"
|
||||
max-height="460"
|
||||
@selection-change="onItemSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
@@ -233,77 +235,130 @@
|
||||
<el-dialog
|
||||
v-model="itemFormVisible"
|
||||
:title="itemFormTitle"
|
||||
width="640px"
|
||||
width="840px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="itemFormRef" :model="itemForm" :rules="itemFormRules" label-position="top" class="biz-dialog-form">
|
||||
<el-form
|
||||
ref="itemFormRef"
|
||||
:model="itemForm"
|
||||
:rules="itemFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form item-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<div class="section-title-left"><h3>基本信息</h3></div>
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="项目编码" prop="itemCode">
|
||||
<el-input v-model="itemForm.itemCode" :disabled="itemIsEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="itemName">
|
||||
<el-input v-model="itemForm.itemName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用(元)" prop="feeAmount">
|
||||
<el-input-number v-model="itemForm.feeAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="itemForm.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section param-config-section">
|
||||
<div class="section-title">
|
||||
<div class="section-title-left"><h3>参数配置</h3></div>
|
||||
<el-button class="btn-main" type="primary" size="small" @click="addItemParam">
|
||||
<el-icon><Plus /></el-icon>添加参数
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="itemParams.length > 0"
|
||||
:data="itemParams"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(i) => i + 1"
|
||||
/>
|
||||
<el-table-column label="参数名称" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramName" placeholder="参数名称" class="inner-cell-input" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数编码" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.paramCode" placeholder="参数编码" class="inner-cell-input" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.paramUnit"
|
||||
placeholder="单位"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
class="inner-cell-select"
|
||||
:loading="unitOptionsLoading"
|
||||
:remote-method="searchUnitOptions"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in unitOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.paramType" class="inner-cell-select">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="数字" value="number" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="必填" width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.requiredFlag" active-value="1" inactive-value="0" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" size="small" @click="removeItemParam($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty
|
||||
v-else
|
||||
description="暂无参数,点击上方按钮添加"
|
||||
:image-size="48"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="项目编码" prop="itemCode">
|
||||
<el-input v-model="itemForm.itemCode" :disabled="itemIsEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="itemName">
|
||||
<el-input v-model="itemForm.itemName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用(元)" prop="feeAmount">
|
||||
<el-input-number v-model="itemForm.feeAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="itemForm.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section" style="margin-top: 8px">
|
||||
<div class="section-title">参数配置</div>
|
||||
<div class="param-row param-row-header">
|
||||
<span style="flex: 2">参数名称</span>
|
||||
<span style="flex: 1.5">参数编码</span>
|
||||
<span style="flex: 1">单位</span>
|
||||
<span style="flex: 1">类型</span>
|
||||
<span style="width: 40px; text-align: center">必填</span>
|
||||
<span style="width: 32px"></span>
|
||||
</div>
|
||||
<div v-if="itemParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
|
||||
暂无参数,点击下方按钮添加
|
||||
</div>
|
||||
<div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
|
||||
<el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
|
||||
<el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
|
||||
<el-select
|
||||
v-model="param.paramUnit"
|
||||
placeholder="单位"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:loading="unitOptionsLoading"
|
||||
:remote-method="searchUnitOptions"
|
||||
style="flex: 1"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in unitOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="param.paramType" style="flex: 1">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="数字" value="number" />
|
||||
</el-select>
|
||||
<el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
|
||||
<el-button class="btn-op-item" @click="removeItemParam(idx)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
<el-button type="primary" plain style="margin-top: 8px" @click="addItemParam"><el-icon><Plus /></el-icon>添加参数</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="itemFormVisible = false">
|
||||
@@ -747,37 +802,86 @@
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
.biz-dialog-form .form-section {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 4px 0;
|
||||
padding: 16px 20px;
|
||||
margin: 4px 0 16px;
|
||||
}
|
||||
|
||||
.form-section .section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
.biz-dialog-form .section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.param-row-header {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 10px;
|
||||
.biz-dialog-form .section-title--static {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.biz-dialog-form .section-title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: #10b981;
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.item-dialog-form {
|
||||
.form-section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.inner-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(td.el-table__cell) {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.inner-cell-input) {
|
||||
.el-input__wrapper {
|
||||
min-height: 36px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner-cell-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 检测项目配置弹窗 teleport 到 body,尺寸类名需非 scoped -->
|
||||
<style lang="scss">
|
||||
.el-dialog.third-party-item-config-dialog.dialog-form-layout {
|
||||
max-height: calc(100vh);
|
||||
min-height: min(740px, calc(100vh - 80px));
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="1200px"
|
||||
width="1380px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
|
||||
Reference in new issue
Block a user