This commit is contained in:
zlwu committed 2026-05-21 11:09:29 +08:00
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