提交
This commit is contained in:
1 parent
28f233c922
commit
038a60b09e
478 files changed
+67530
-51951
No files matched your search
@@ -0,0 +1,118 @@
|
||||
<!-- 附件 ZIP 导出进度弹窗 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="正在导出附件"
|
||||
width="480px"
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="!running"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<div class="attachment-zip-progress">
|
||||
<el-progress :percentage="displayPercent" :stroke-width="8" :status="progressStatus" />
|
||||
<p class="attachment-zip-progress__phase">{{ phaseLabel }}</p>
|
||||
<p v-if="progress.currentFile" class="attachment-zip-progress__file">
|
||||
{{ progress.currentFile }}
|
||||
</p>
|
||||
<p v-if="progress.contextHint" class="attachment-zip-progress__hint">
|
||||
{{ progress.contextHint }}
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button v-if="running" class="btn-ghost" @click="emit('cancel')">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button v-else class="btn-main" type="primary" @click="dialogVisible = false">
|
||||
<el-icon><Check /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Close, Check } from '@element-plus/icons-vue'
|
||||
import type { AttachmentZipExportProgress } from '@/utils/qc-work-order-attachment-export'
|
||||
|
||||
defineOptions({ name: 'AttachmentZipProgressDialog' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
progress: AttachmentZipExportProgress
|
||||
running?: boolean
|
||||
}>(),
|
||||
{
|
||||
running: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const displayPercent = computed(() => Math.min(100, Math.max(0, props.progress.percent || 0)))
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
if (props.progress.phase === 'done') return 'success'
|
||||
if (props.progress.phase === 'cancelled') return 'warning'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const phaseLabel = computed(() => {
|
||||
const { phase, currentIndex, totalFiles } = props.progress
|
||||
if (phase === 'preparing') return '正在加载工单附件清单…'
|
||||
if (phase === 'downloading') {
|
||||
const total = totalFiles ? `(${currentIndex || 0}/${totalFiles})` : ''
|
||||
return `正在下载附件${total}`
|
||||
}
|
||||
if (phase === 'zipping') return '正在生成压缩包…'
|
||||
if (phase === 'done') return '导出完成'
|
||||
if (phase === 'cancelled') return '已取消导出'
|
||||
return '处理中…'
|
||||
})
|
||||
|
||||
function handleDialogClose() {
|
||||
if (props.running) {
|
||||
emit('cancel')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attachment-zip-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.attachment-zip-progress__phase {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
|
||||
.attachment-zip-progress__file {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-secondary, #334155);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.attachment-zip-progress__hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
:type="protocolStatusTagType(String(detail?.protocolStatus || ''))"
|
||||
size="small"
|
||||
>
|
||||
{{ protocolStatusLabel(String(detail?.protocolStatus || '')) }}
|
||||
{{ protocolStatusDisplayLabel(String(detail?.protocolStatus || ''), String(detail?.voidType || '')) }}
|
||||
</el-tag>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@
|
||||
payModeLabel,
|
||||
payStatusLabel as protocolPayStatusLabel,
|
||||
payStatusTagType as protocolPayStatusTagType,
|
||||
protocolStatusLabel,
|
||||
protocolStatusLabel, protocolStatusDisplayLabel,
|
||||
protocolStatusTagType
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
@@ -194,7 +194,11 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="320">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="paper-att-cards">
|
||||
<div
|
||||
class="paper-att-cards paper-att-list"
|
||||
:class="{ 'is-dragover': isPaperRowDragover($index) }"
|
||||
v-bind="paperRowDropHandlers($index)"
|
||||
>
|
||||
<div
|
||||
v-for="(att, attIdx) in row.attachments"
|
||||
:key="attIdx"
|
||||
@@ -405,6 +409,7 @@
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { useIndexedAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import { fetchCellProductList } from '@/api/cell-product'
|
||||
import { fetchMaterialList, type MaterialOption } from '@/api/material'
|
||||
import type { OutboundReleaseCellRow } from '@/api/outbound-release-review'
|
||||
@@ -886,6 +891,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
const { isDragover: isPaperRowDragover, handlersFor: paperRowDropHandlers } =
|
||||
useIndexedAttachmentDropZone({
|
||||
onFiles: (index, files) => {
|
||||
for (const file of files) {
|
||||
void handlePaperAttachmentChange(index, { raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loading,
|
||||
loadFromApiData,
|
||||
|
||||
@@ -724,21 +724,22 @@
|
||||
|
||||
.view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr;
|
||||
gap: 8px 12px;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-kv dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-model:visible="innerVisible"
|
||||
title="生产任务详情"
|
||||
width="960px"
|
||||
:grid-columns="3"
|
||||
:grid-columns="4"
|
||||
:icon="Operation"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="attachmentLabel" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{ 'is-dragover': isAttachDragover }"
|
||||
v-bind="attachDropHandlers"
|
||||
>
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
@@ -70,7 +74,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="upload-hint">支持图片、视频、文档等。</p>
|
||||
<p class="upload-hint">
|
||||
支持拖拽或点击上传图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -81,6 +87,7 @@
|
||||
import { Plus, Document, Delete, Download, Loading, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import type { FinanceAttachmentItem } from './finance-attachments'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -193,6 +200,15 @@
|
||||
emit('update:attachments', remaining)
|
||||
}
|
||||
}
|
||||
|
||||
const { isDragover: isAttachDragover, dropZoneHandlers: attachDropHandlers } =
|
||||
useAttachmentDropZone({
|
||||
onFiles: (files) => {
|
||||
for (const file of files) {
|
||||
void handleAttachmentChange({ raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>协议状态</dt>
|
||||
<dd>{{ protocolStatusLabel(String(detail?.protocolStatus || '')) }}</dd>
|
||||
<dd>{{ protocolStatusDisplayLabel(String(detail?.protocolStatus || ''), String(detail?.voidType || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>缴费方式</dt>
|
||||
@@ -112,6 +112,7 @@
|
||||
payStatusLabel,
|
||||
payStatusTagType,
|
||||
protocolStatusLabel,
|
||||
protocolStatusDisplayLabel,
|
||||
loadTransferTypeDict,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-model:visible="innerVisible"
|
||||
title="质检任务详情"
|
||||
width="960px"
|
||||
:grid-columns="3"
|
||||
:grid-columns="4"
|
||||
:icon="Tickets"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
@@ -14,6 +14,14 @@
|
||||
compact
|
||||
modal-class="qc-task-view-dialog-mask"
|
||||
>
|
||||
<template v-if="viewSampleSpecial.isSpecial" #prepend>
|
||||
<SampleCodeWithSpecialDisplay
|
||||
:sample-code="viewSampleCode"
|
||||
:is-special="viewSampleSpecial.isSpecial"
|
||||
:special-remark="viewSampleSpecial.specialRemark"
|
||||
monospace
|
||||
/>
|
||||
</template>
|
||||
<template #reception-extra>
|
||||
<div class="view-reception-attachments">
|
||||
<div class="view-kv view-kv-span-2">
|
||||
@@ -94,6 +102,7 @@
|
||||
import { Tickets } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchQcTaskDetail, type QcTask, type QcTaskDetail } from '@/api/qc-task'
|
||||
import {
|
||||
@@ -114,7 +123,6 @@
|
||||
workOrderStatusLabel,
|
||||
qcTaskConclusionLabel
|
||||
} from '@/utils/qc-labels'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { loadQcInspectionBatches, type QcInspectionBatch } from '@/utils/qc-inspection-batch'
|
||||
import {
|
||||
qcAttachmentFilesFromCommaUrls,
|
||||
@@ -293,6 +301,23 @@
|
||||
return qcAttachmentFilesFromCommaUrls(String(d.thirdPartyAttachments || ''))
|
||||
})
|
||||
|
||||
const viewSampleSpecial = computed(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return { isSpecial: false, specialRemark: '' }
|
||||
const r = (d.reception || {}) as Record<string, unknown>
|
||||
return {
|
||||
isSpecial: !!(d.isSpecial || r.isSpecial),
|
||||
specialRemark: String(d.specialRemark || r.specialRemark || '').trim()
|
||||
}
|
||||
})
|
||||
|
||||
const viewSampleCode = computed(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return ''
|
||||
const r = (d.reception || {}) as Record<string, unknown>
|
||||
return String(r.sampleCode || d.sampleCode || '').trim()
|
||||
})
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
return String(v)
|
||||
@@ -410,13 +435,7 @@
|
||||
{ label: '项目名称', value: d.itemName || '—' },
|
||||
{ label: '项目编码', value: d.itemCode || '—' },
|
||||
...(d.taskType === 'dsfjc'
|
||||
? [
|
||||
{ label: '第三方公司', value: d.companyName || '—' },
|
||||
{
|
||||
label: '检测费用(元)',
|
||||
value: d.feeAmount != null ? `¥${formatMoneyDisplay(d.feeAmount)}` : '—'
|
||||
}
|
||||
]
|
||||
? [{ label: '第三方公司', value: d.companyName || '—' }]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
@@ -511,14 +530,15 @@
|
||||
margin-top: 4px;
|
||||
|
||||
.view-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
align-items: start;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- 样本编码 + 特殊样本警示(列表场景:编码与说明均为红色加粗) -->
|
||||
<template>
|
||||
<span
|
||||
class="sample-code-with-special"
|
||||
:class="[
|
||||
`sample-code-with-special--${layout}`,
|
||||
{ 'sample-code-with-special--danger': isSpecial },
|
||||
{ 'sample-code-with-special--mono': monospace }
|
||||
]"
|
||||
>
|
||||
<span v-if="showCode" class="sample-code-with-special__code">{{ displayCode }}</span>
|
||||
<SpecialSampleDisplay
|
||||
v-if="isSpecial"
|
||||
:is-special="isSpecial"
|
||||
:special-remark="specialRemark"
|
||||
:layout="layout"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SpecialSampleDisplay from '@/components/business/special-sample-display/index.vue'
|
||||
|
||||
defineOptions({ name: 'SampleCodeWithSpecialDisplay' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sampleCode?: string
|
||||
isSpecial?: boolean
|
||||
specialRemark?: string
|
||||
layout?: 'inline' | 'block'
|
||||
fallback?: string
|
||||
showCode?: boolean
|
||||
monospace?: boolean
|
||||
}>(),
|
||||
{
|
||||
sampleCode: '',
|
||||
isSpecial: false,
|
||||
specialRemark: '',
|
||||
layout: 'inline',
|
||||
fallback: '—',
|
||||
showCode: true,
|
||||
monospace: false
|
||||
}
|
||||
)
|
||||
|
||||
const displayCode = computed(() => {
|
||||
const code = String(props.sampleCode || '').trim()
|
||||
return code || props.fallback
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sample-code-with-special {
|
||||
&.sample-code-with-special--inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&.sample-code-with-special--block {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sample-code-with-special--mono .sample-code-with-special__code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sample-code-with-special--danger .sample-code-with-special__code {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div v-if="showSectionLabel" class="sample-card__section-label">质检信息</div>
|
||||
<div class="sample-qc-grouped-panel">
|
||||
<section class="sample-qc-grouped-panel__block">
|
||||
<section v-if="showGateSection" class="sample-qc-grouped-panel__block">
|
||||
<h4 class="sample-qc-grouped-panel__subtitle">{{ gateSectionLabel }}</h4>
|
||||
<SampleQcInfoCards
|
||||
embedded
|
||||
@@ -42,7 +42,11 @@
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<SampleQcInfoCards embedded :qc-info="item" :hide-sample-context="hideSampleContext" />
|
||||
<SampleQcInfoCards
|
||||
embedded
|
||||
:qc-info="item"
|
||||
:hide-sample-context="hideSampleContext"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -66,12 +70,15 @@
|
||||
hideSampleContext?: boolean
|
||||
/** 协议/捐赠样本卡内展示外层「质检信息」标题 */
|
||||
showSectionLabel?: boolean
|
||||
/** 是否展示样本接收质检(入库门禁)区块 */
|
||||
showGateSection?: boolean
|
||||
gateSectionLabel?: string
|
||||
processSectionLabel?: string
|
||||
}>(),
|
||||
{
|
||||
processQcInfoList: () => [],
|
||||
showSectionLabel: true,
|
||||
showGateSection: true,
|
||||
gateSectionLabel: '样本接收质检(入库门禁)',
|
||||
processSectionLabel: '生产过程质检'
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h4 v-if="qcTitle" class="detail-section-title">{{ qcTitle }}</h4>
|
||||
<div v-if="!qcReception" class="sample-reception-dual-readonly__empty">{{ qcEmptyHint }}</div>
|
||||
<template v-else>
|
||||
<div class="readonly-grid form-grid-2">
|
||||
<div class="readonly-grid form-grid-4">
|
||||
<div class="readonly-item">
|
||||
<span class="label">接收时间</span>{{ formatDisplayOrDash(qcReception.receptionTime) }}
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
showProduction: true,
|
||||
qcTitle: '质检接收信息',
|
||||
productionTitle: '生产接收信息',
|
||||
productionGridCols: 2,
|
||||
productionGridCols: 4,
|
||||
qcEmptyHint: '暂未接收',
|
||||
productionEmptyHint: '生产尚未登记'
|
||||
}
|
||||
@@ -198,19 +198,30 @@
|
||||
}
|
||||
|
||||
.readonly-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
|
||||
&::after {
|
||||
content: ': ';
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .el-tag {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.check-items-readonly {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- 入库样本 · 特殊标记与特殊说明编辑 -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="调整特殊样本"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="样本编码" class="form-col-span-2">
|
||||
<span class="sample-special-mark-dialog__code">{{ sampleCode || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-col-span-2">
|
||||
<el-checkbox v-model="form.isSpecial">特殊样本</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item label="特殊说明" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.specialRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
placeholder="如小三阳、HBV 携带等"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="handleClose">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchUpdateInboundSampleSpecialMark } from '@/api/inbound-sample'
|
||||
import type { InboundSampleSpecialMarkTarget } from '@/utils/inbound-sample-special-mark'
|
||||
|
||||
defineOptions({ name: 'SampleSpecialMarkDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
target: InboundSampleSpecialMarkTarget | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [visible: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const sampleCode = ref('')
|
||||
const form = reactive({
|
||||
isSpecial: false,
|
||||
specialRemark: ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
dialogVisible.value = visible
|
||||
if (visible) syncFromTarget()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(dialogVisible, (visible) => {
|
||||
emit('update:visible', visible)
|
||||
})
|
||||
|
||||
function syncFromTarget() {
|
||||
const target = props.target
|
||||
sampleCode.value = target?.sampleCode || ''
|
||||
form.isSpecial = !!target?.isSpecial
|
||||
form.specialRemark = target?.specialRemark || ''
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const target = props.target
|
||||
if (!target?.protocolSampleId) {
|
||||
ElMessage.warning('缺少样本信息,无法保存')
|
||||
return
|
||||
}
|
||||
if (form.isSpecial && !String(form.specialRemark || '').trim()) {
|
||||
ElMessage.warning('特殊样本须填写特殊说明')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await fetchUpdateInboundSampleSpecialMark({
|
||||
receptionSource: target.receptionSource,
|
||||
protocolSampleId: target.protocolSampleId,
|
||||
isSpecial: form.isSpecial,
|
||||
specialRemark: String(form.specialRemark || '').trim()
|
||||
})
|
||||
ElMessage.success('特殊样本已更新')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sample-special-mark-dialog__code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
</style>
|
||||
@@ -35,8 +35,15 @@
|
||||
<el-table-column label="样本类型" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ sampleTypeName(row.sampleTypeId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="样本编码" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.sampleCode || '—' }}</template>
|
||||
<el-table-column label="样本编码" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<SampleCodeWithSpecialDisplay
|
||||
:sample-code="row.sampleCode"
|
||||
:is-special="!!row.isSpecial"
|
||||
:special-remark="row.specialRemark"
|
||||
monospace
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showPrice"
|
||||
@@ -73,6 +80,12 @@
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="特殊说明" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.isSpecial">{{ row.specialRemark || '—' }}</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运输类型" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
@@ -220,6 +233,22 @@
|
||||
<el-option label="已作废" value="yzf" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-col-span-2">
|
||||
<el-checkbox v-model="sampleDraft.isSpecial" @change="onSpecialSampleToggle">
|
||||
特殊样本
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item label="特殊说明" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="sampleDraft.specialRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:disabled="!sampleDraft.isSpecial"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
placeholder="如小三阳、HBV 携带等"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -361,6 +390,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Plus, Edit, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import {
|
||||
sampleStatusLabel,
|
||||
sampleStatusTagType,
|
||||
@@ -471,6 +501,8 @@
|
||||
return {
|
||||
...raw,
|
||||
sampleStatus,
|
||||
isSpecial: !!raw.isSpecial,
|
||||
specialRemark: String(raw.specialRemark || '').trim(),
|
||||
_rowKey: raw.id || raw._rowKey || `new-${++sampleRowSeq}`,
|
||||
transport: {
|
||||
...createEmptyTransport(),
|
||||
@@ -708,6 +740,12 @@
|
||||
ElMessage.success(`已为 ${selectedSamples.value.length} 个样本应用运输信息`)
|
||||
}
|
||||
|
||||
function onSpecialSampleToggle(checked: boolean | string | number) {
|
||||
if (!checked) {
|
||||
sampleDraft.specialRemark = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function openAddSampleDialog() {
|
||||
sampleDialogMode.value = 'add'
|
||||
sampleDialogIndex.value = -1
|
||||
@@ -723,7 +761,9 @@
|
||||
price: props.showPrice ? null : undefined,
|
||||
originalPrice: props.showPrice ? null : undefined,
|
||||
storageYears: null,
|
||||
sampleStatus: 'ycj'
|
||||
sampleStatus: 'ycj',
|
||||
isSpecial: false,
|
||||
specialRemark: ''
|
||||
},
|
||||
props.samples.length
|
||||
)
|
||||
@@ -778,6 +818,13 @@
|
||||
ElMessage.warning('请选择样本编码')
|
||||
return
|
||||
}
|
||||
if (sampleDraft.isSpecial && !String(sampleDraft.specialRemark || '').trim()) {
|
||||
ElMessage.warning('特殊样本须填写特殊说明')
|
||||
return
|
||||
}
|
||||
if (!sampleDraft.isSpecial) {
|
||||
sampleDraft.specialRemark = ''
|
||||
}
|
||||
ensureSampleOriginalPrice(sampleDraft)
|
||||
const next = [...props.samples]
|
||||
const normalized = normalizeSampleRow(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- 特殊样本警示展示(红色加粗) -->
|
||||
<template>
|
||||
<span v-if="isSpecial" class="special-sample-display" :class="`special-sample-display--${layout}`">
|
||||
<span class="special-sample-mark">【特殊样本】</span>
|
||||
<span v-if="displayRemark" class="special-sample-remark">{{ displayRemark }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'SpecialSampleDisplay' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isSpecial?: boolean
|
||||
specialRemark?: string
|
||||
layout?: 'inline' | 'block'
|
||||
}>(),
|
||||
{
|
||||
isSpecial: false,
|
||||
specialRemark: '',
|
||||
layout: 'inline'
|
||||
}
|
||||
)
|
||||
|
||||
const displayRemark = computed(() => String(props.specialRemark || '').trim())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.special-sample-display {
|
||||
&.special-sample-display--inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&.special-sample-display--block {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.special-sample-mark,
|
||||
.special-sample-remark {
|
||||
color: var(--el-color-danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.special-sample-remark {
|
||||
margin-left: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -21,10 +21,17 @@
|
||||
</div>
|
||||
<div class="view-hero-main">
|
||||
<h2>{{ title }}</h2>
|
||||
<p v-if="heroMeta" class="view-hero-meta">{{ heroMeta }}</p>
|
||||
<p v-if="heroMeta && !$slots['hero-meta']" class="view-hero-meta">{{ heroMeta }}</p>
|
||||
<div v-if="$slots['hero-meta']" class="view-hero-meta">
|
||||
<slot name="hero-meta" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.prepend" class="view-prepend">
|
||||
<slot name="prepend" />
|
||||
</div>
|
||||
|
||||
<!-- sections -->
|
||||
<div
|
||||
v-for="(section, si) in sections"
|
||||
@@ -298,6 +305,12 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-prepend {
|
||||
padding: 12px 22px;
|
||||
background: #fff5f5;
|
||||
border-bottom: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.view-hero-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
@@ -385,14 +398,16 @@
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
align-items: start;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -401,6 +416,11 @@
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
|
||||
> .el-tag {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,11 +508,6 @@
|
||||
.view-kv-grid--cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.system-view-dialog--compact {
|
||||
@@ -539,7 +554,10 @@
|
||||
|
||||
.view-kv {
|
||||
font-size: 12px;
|
||||
grid-template-columns: 84px 1fr;
|
||||
|
||||
dt {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-field-label {
|
||||
|
||||
Reference in new issue
Block a user