提交
This commit is contained in:
1 parent
8ff159c251
commit
c1832f9d03
60 files changed
+13400
-10821
No files matched your search
+19
-431
@@ -21,156 +21,10 @@
|
||||
|
||||
<div class="outbound-chain-section">
|
||||
<div class="section-subtitle">出库物料准备</div>
|
||||
<div class="outbound-material-view">
|
||||
<div v-if="!hasOutboundMaterial" class="outbound-material-empty">暂无出库物料准备记录</div>
|
||||
<template v-else>
|
||||
<div v-if="outboundMaterialCells.length" class="outbound-material-block">
|
||||
<h4 class="outbound-material-subtitle">出库细胞</h4>
|
||||
<el-table
|
||||
:data="outboundMaterialCells"
|
||||
class="list-table inner-table"
|
||||
stripe
|
||||
size="small"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column
|
||||
prop="productCode"
|
||||
label="产品编码"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productName"
|
||||
label="产品名称"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="productTypeName" label="类型" min-width="90" />
|
||||
<el-table-column prop="specName" label="规格" min-width="90" />
|
||||
<el-table-column
|
||||
prop="storageSlotName"
|
||||
label="储物格"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.productStatus"
|
||||
:type="cellProductStatusTagType(row.productStatus) as any"
|
||||
size="small"
|
||||
>
|
||||
{{ cellProductStatusLabel(row.productStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="outboundMaterialQcRows.length" class="outbound-material-block">
|
||||
<h4 class="outbound-material-subtitle">使用的物料</h4>
|
||||
<el-table
|
||||
:data="outboundMaterialQcRows"
|
||||
class="list-table inner-table"
|
||||
stripe
|
||||
size="small"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column
|
||||
prop="materialName"
|
||||
label="物料名称"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="110" />
|
||||
<el-table-column label="计划用量" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" min-width="72" align="center" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-if="outboundPaperRows.length" class="outbound-material-block">
|
||||
<h4 class="outbound-material-subtitle">其他纸质材料</h4>
|
||||
<el-table
|
||||
:data="outboundPaperRows"
|
||||
class="list-table inner-table"
|
||||
stripe
|
||||
size="small"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="份数" min-width="80" align="center">
|
||||
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
|
||||
<div
|
||||
v-for="(att, attIdx) in parsePaperAttachments(row.attachmentUrl)"
|
||||
:key="attIdx"
|
||||
class="paper-att-card"
|
||||
:class="{
|
||||
'paper-att-card--image': isImageFile(att),
|
||||
'paper-att-card--video': isVideoFile(att)
|
||||
}"
|
||||
:title="att.fileName"
|
||||
>
|
||||
<div
|
||||
class="paper-att-card__preview"
|
||||
:class="{
|
||||
'is-clickable': isImageFile(att) || isVideoFile(att)
|
||||
}"
|
||||
@click="
|
||||
(isImageFile(att) || isVideoFile(att)) &&
|
||||
handlePreviewPaperAttachment(att)
|
||||
"
|
||||
>
|
||||
<img
|
||||
v-if="isImageFile(att)"
|
||||
:src="attachmentFullUrl(att.attachmentUrl)"
|
||||
:alt="att.fileName || '附件'"
|
||||
/>
|
||||
<template v-else-if="isVideoFile(att)">
|
||||
<div class="paper-att-card__type-icon paper-att-card__type-icon--video">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="paper-att-card__type-icon paper-att-card__type-icon--doc">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span class="paper-att-card__name">{{ att.fileName || '附件' }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="paper-att-card__actions">
|
||||
<el-icon
|
||||
v-if="isImageFile(att) || isVideoFile(att)"
|
||||
class="paper-att-card__action"
|
||||
title="预览"
|
||||
@click.stop="handlePreviewPaperAttachment(att)"
|
||||
>
|
||||
<View />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="paper-att-card__action"
|
||||
title="下载"
|
||||
@click.stop="handleDownloadPaperAttachment(att)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<p v-if="outboundMaterialRemark" class="outbound-material-remark">
|
||||
备注:{{ outboundMaterialRemark }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<OutboundMaterialReadonlyPanel
|
||||
:material="outboundMaterial"
|
||||
:biz-type="scheduleBizType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="outbound-chain-section">
|
||||
@@ -278,7 +132,7 @@
|
||||
<div class="data-item data-item--full">
|
||||
<div class="data-label">说明</div>
|
||||
<div class="data-value data-value--muted">
|
||||
已在输注计划管理中确认,暂无独立应用确认记录
|
||||
已在{{ scheduleBizConfig.scheduleLabel }}管理中确认,暂无独立应用确认记录
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -291,48 +145,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
style="backdrop-filter: none"
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
alt="预览"
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="preview-container">
|
||||
<video
|
||||
:src="previewUrl"
|
||||
controls
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Document, Download, View, VideoPlay } from '@element-plus/icons-vue'
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||||
import OutboundMaterialReadonlyPanel from '@/components/business/outbound-material-readonly-panel/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { fetchInfusionScheduleMgmtDetail } from '@/api/admin'
|
||||
import { getPilotBizConfig, resolvePilotBizType } from '@/config/pilot-biz-type'
|
||||
import { resolveOutboundViewVariant } from '@/utils/outbound-qc-pilot-view-sections'
|
||||
import {
|
||||
storageReleaseConclusionLabel,
|
||||
storageReleaseConclusionTagType,
|
||||
infusionStatusLabel,
|
||||
infusionStatusTagType,
|
||||
cellProductStatusLabel,
|
||||
cellProductStatusTagType
|
||||
infusionStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
@@ -355,60 +184,26 @@
|
||||
|
||||
const loading = ref(false)
|
||||
const fetchedDetail = ref<Record<string, any> | null>(null)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
|
||||
type PaperAttachmentView = {
|
||||
fileName: string
|
||||
attachmentUrl: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
}
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const detail = computed(() => props.detailData || fetchedDetail.value)
|
||||
|
||||
const scheduleBizConfig = computed(() =>
|
||||
getPilotBizConfig(
|
||||
resolvePilotBizType(detail.value?.application?.bizType || detail.value?.bizType)
|
||||
)
|
||||
)
|
||||
|
||||
const scheduleBizType = computed(() =>
|
||||
resolveOutboundViewVariant(detail.value?.application?.bizType || detail.value?.bizType)
|
||||
)
|
||||
|
||||
const review = computed(() => detail.value?.reviewInfo || {})
|
||||
const outboundHandover = computed(() => detail.value?.outboundHandover || {})
|
||||
const infusionConfirm = computed(() => detail.value?.infusionConfirm || {})
|
||||
const outboundMaterial = computed(() => detail.value?.outboundMaterial || null)
|
||||
|
||||
const outboundMaterialCells = computed(() => outboundMaterial.value?.cells || [])
|
||||
const outboundMaterialQcRows = computed(() => outboundMaterial.value?.qcMaterials || [])
|
||||
const outboundMaterialRemark = computed(() => outboundMaterial.value?.remark || '')
|
||||
|
||||
const outboundPaperRows = computed(() => {
|
||||
const materials = outboundMaterial.value?.paperMaterials || []
|
||||
if (materials.length) {
|
||||
return materials.map((p: Record<string, any>) => ({
|
||||
paperName: p.paperName || '',
|
||||
copies: p.copies,
|
||||
attachmentUrl: p.attachmentUrl || ''
|
||||
}))
|
||||
}
|
||||
const urls = outboundMaterial.value?.paperAttachments || ''
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u: string) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((attachmentUrl: string) => ({
|
||||
paperName: paperAttachmentName(attachmentUrl),
|
||||
copies: 1,
|
||||
attachmentUrl
|
||||
}))
|
||||
})
|
||||
|
||||
const hasOutboundMaterial = computed(
|
||||
() =>
|
||||
outboundMaterialCells.value.length > 0 ||
|
||||
outboundMaterialQcRows.value.length > 0 ||
|
||||
outboundPaperRows.value.length > 0 ||
|
||||
!!outboundMaterialRemark.value.trim()
|
||||
)
|
||||
|
||||
const reviewAttachmentFiles = computed(() => {
|
||||
const urls = review.value?.reviewAttachments || ''
|
||||
if (!urls.trim()) return []
|
||||
@@ -464,46 +259,6 @@
|
||||
return !!(confirm?.id || confirm?.confirmTime || confirm?.confirmUserName)
|
||||
}
|
||||
|
||||
function paperAttachmentName(url: string) {
|
||||
return url.split('/').pop() || '附件'
|
||||
}
|
||||
|
||||
function splitUrls(str: string): string[] {
|
||||
if (!str?.trim()) return []
|
||||
return str
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function parsePaperAttachments(urlStr: string): PaperAttachmentView[] {
|
||||
return splitUrls(urlStr).map((url) => ({
|
||||
fileName: paperAttachmentName(url),
|
||||
attachmentUrl: url,
|
||||
fileUrl: url
|
||||
}))
|
||||
}
|
||||
|
||||
function isImageFile(file: PaperAttachmentView) {
|
||||
const url = (file.fileUrl || file.attachmentUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('image/')
|
||||
)
|
||||
}
|
||||
|
||||
function isVideoFile(file: PaperAttachmentView) {
|
||||
const url = (file.fileUrl || file.attachmentUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
|
||||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('video/')
|
||||
)
|
||||
}
|
||||
|
||||
function attachmentFullUrl(url: string) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return ''
|
||||
@@ -511,25 +266,6 @@
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
|
||||
function handlePreviewPaperAttachment(att: PaperAttachmentView) {
|
||||
if (!att.attachmentUrl) return
|
||||
previewUrl.value = attachmentFullUrl(att.attachmentUrl)
|
||||
previewType.value = isVideoFile(att) ? 'video' : 'image'
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDownloadPaperAttachment(att: PaperAttachmentView) {
|
||||
if (!att.attachmentUrl) return
|
||||
downloadAttachmentFile(
|
||||
{
|
||||
fileName: att.fileName,
|
||||
fileUrl: att.attachmentUrl,
|
||||
fileType: att.fileType || ''
|
||||
},
|
||||
{ fullFileUrl: attachmentFullUrl }
|
||||
)
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = attachmentFullUrl(String(file.fileUrl || ''))
|
||||
if (url) window.open(url, '_blank')
|
||||
@@ -592,36 +328,6 @@
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.outbound-material-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outbound-material-empty {
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.outbound-material-block {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-material-subtitle {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.outbound-material-remark {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.attachment-row {
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -667,122 +373,4 @@
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.paper-att-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.paper-att-card {
|
||||
width: 100px;
|
||||
height: 72px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.paper-att-card__preview {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card--image,
|
||||
.paper-att-card--video {
|
||||
.paper-att-card__preview {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card__type-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&--doc {
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&--video {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card__name {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.paper-att-card__actions {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.paper-att-card:hover .paper-att-card__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.paper-att-card__action {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #047857;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,536 @@
|
||||
<!-- 出库物料准备 — 只读展示(出库细胞 / 物料 / 纸质材料) -->
|
||||
<template>
|
||||
<div class="outbound-material-readonly-panel">
|
||||
<p v-if="!hasContent" class="view-empty-hint">{{ emptyText }}</p>
|
||||
<template v-else>
|
||||
<div v-if="cells.length" class="outbound-material-block">
|
||||
<div class="section-subtitle">出库细胞</div>
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
:data="cells"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="320"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productCode"
|
||||
label="产品编码"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productName"
|
||||
label="产品名称"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productTypeName"
|
||||
label="类型"
|
||||
min-width="90"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="specName" label="规格" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
prop="storageSlotName"
|
||||
label="储物格"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="outboundMaterialCellStatusTagType(row.productStatus || '', bizType) as any"
|
||||
size="small"
|
||||
>
|
||||
{{ outboundMaterialCellStatusLabel(row.productStatus || '', bizType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="qcMaterials.length" class="outbound-material-block">
|
||||
<div class="section-subtitle">使用的物料</div>
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
:data="qcMaterials"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="320"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="materialName"
|
||||
label="物料名称"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="计划用量" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="unit"
|
||||
label="单位"
|
||||
min-width="72"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paperRows.length" class="outbound-material-block">
|
||||
<div class="section-subtitle">其他纸质材料</div>
|
||||
<div class="list-body list-wrap view-inner-table-wrap">
|
||||
<el-table
|
||||
:data="paperRows"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="320"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="份数" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
|
||||
<div
|
||||
v-for="(att, attIdx) in parsePaperAttachments(row.attachmentUrl)"
|
||||
:key="attIdx"
|
||||
class="paper-att-card"
|
||||
:class="{
|
||||
'paper-att-card--image': isImageFile(att),
|
||||
'paper-att-card--video': isVideoFile(att)
|
||||
}"
|
||||
:title="att.fileName"
|
||||
>
|
||||
<div
|
||||
class="paper-att-card__preview"
|
||||
:class="{
|
||||
'is-clickable': isImageFile(att) || isVideoFile(att)
|
||||
}"
|
||||
@click="
|
||||
(isImageFile(att) || isVideoFile(att)) &&
|
||||
handlePreviewPaperAttachment(att)
|
||||
"
|
||||
>
|
||||
<img
|
||||
v-if="isImageFile(att)"
|
||||
:src="attachmentFullUrl(att.attachmentUrl)"
|
||||
:alt="att.fileName || '附件'"
|
||||
/>
|
||||
<template v-else-if="isVideoFile(att)">
|
||||
<div class="paper-att-card__type-icon paper-att-card__type-icon--video">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="paper-att-card__type-icon paper-att-card__type-icon--doc">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span class="paper-att-card__name">{{ att.fileName || '附件' }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="paper-att-card__actions">
|
||||
<el-icon
|
||||
v-if="isImageFile(att) || isVideoFile(att)"
|
||||
class="paper-att-card__action"
|
||||
title="预览"
|
||||
@click.stop="handlePreviewPaperAttachment(att)"
|
||||
>
|
||||
<View />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="paper-att-card__action"
|
||||
title="下载"
|
||||
@click.stop="handleDownloadPaperAttachment(att)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="remark" class="outbound-material-remark">备注:{{ remark }}</p>
|
||||
</template>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
style="backdrop-filter: none"
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
alt="预览"
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="preview-container">
|
||||
<video
|
||||
:src="previewUrl"
|
||||
controls
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { Document, Download, View, VideoPlay } from '@element-plus/icons-vue'
|
||||
import type {
|
||||
OutboundReleaseCellRow,
|
||||
OutboundReleaseMaterialDetail,
|
||||
OutboundReleaseQcMaterialRow
|
||||
} from '@/api/outbound-release-review'
|
||||
import {
|
||||
outboundMaterialCellStatusLabel,
|
||||
outboundMaterialCellStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
defineOptions({ name: 'OutboundMaterialReadonlyPanel' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
material?: OutboundReleaseMaterialDetail | Record<string, any> | null
|
||||
bizType?: string
|
||||
emptyText?: string
|
||||
}>(),
|
||||
{
|
||||
material: null,
|
||||
bizType: '',
|
||||
emptyText: '暂无出库物料准备记录'
|
||||
}
|
||||
)
|
||||
|
||||
type PaperAttachmentView = {
|
||||
fileName: string
|
||||
attachmentUrl: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
}
|
||||
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const cells = computed<OutboundReleaseCellRow[]>(() => props.material?.cells || [])
|
||||
const qcMaterials = computed<OutboundReleaseQcMaterialRow[]>(
|
||||
() => props.material?.qcMaterials || []
|
||||
)
|
||||
const remark = computed(() => String(props.material?.remark || '').trim())
|
||||
|
||||
const paperRows = computed(() => {
|
||||
const materials = props.material?.paperMaterials || []
|
||||
if (materials.length) {
|
||||
return materials.map((p: Record<string, any>) => ({
|
||||
paperName: p.paperName || '',
|
||||
copies: p.copies,
|
||||
attachmentUrl: p.attachmentUrl || ''
|
||||
}))
|
||||
}
|
||||
const urls = props.material?.paperAttachments || ''
|
||||
if (!String(urls).trim()) return []
|
||||
return String(urls)
|
||||
.split(',')
|
||||
.map((u: string) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((attachmentUrl: string) => ({
|
||||
paperName: paperAttachmentName(attachmentUrl),
|
||||
copies: 1,
|
||||
attachmentUrl
|
||||
}))
|
||||
})
|
||||
|
||||
const hasContent = computed(
|
||||
() =>
|
||||
cells.value.length > 0 ||
|
||||
qcMaterials.value.length > 0 ||
|
||||
paperRows.value.length > 0 ||
|
||||
!!remark.value
|
||||
)
|
||||
|
||||
function paperAttachmentName(url: string) {
|
||||
return url.split('/').pop() || '附件'
|
||||
}
|
||||
|
||||
function splitUrls(str: string): string[] {
|
||||
if (!str?.trim()) return []
|
||||
return str
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function parsePaperAttachments(urlStr: string): PaperAttachmentView[] {
|
||||
return splitUrls(urlStr).map((url) => ({
|
||||
fileName: paperAttachmentName(url),
|
||||
attachmentUrl: url,
|
||||
fileUrl: url
|
||||
}))
|
||||
}
|
||||
|
||||
function isImageFile(file: PaperAttachmentView) {
|
||||
const url = (file.fileUrl || file.attachmentUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('image/')
|
||||
)
|
||||
}
|
||||
|
||||
function isVideoFile(file: PaperAttachmentView) {
|
||||
const url = (file.fileUrl || file.attachmentUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
|
||||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('video/')
|
||||
)
|
||||
}
|
||||
|
||||
function attachmentFullUrl(url: string) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
|
||||
function handlePreviewPaperAttachment(att: PaperAttachmentView) {
|
||||
if (!att.attachmentUrl) return
|
||||
previewUrl.value = attachmentFullUrl(att.attachmentUrl)
|
||||
previewType.value = isVideoFile(att) ? 'video' : 'image'
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDownloadPaperAttachment(att: PaperAttachmentView) {
|
||||
if (!att.attachmentUrl) return
|
||||
downloadAttachmentFile(
|
||||
{
|
||||
fileName: att.fileName,
|
||||
fileUrl: att.attachmentUrl,
|
||||
fileType: att.fileType || ''
|
||||
},
|
||||
{ fullFileUrl: attachmentFullUrl }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.outbound-material-readonly-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.outbound-material-block {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.outbound-material-remark {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.view-inner-table-wrap.list-body.list-wrap {
|
||||
margin-top: 0;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||||
--el-table-text-color: var(--mint-text, #0f172a);
|
||||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.paper-att-card {
|
||||
width: 100px;
|
||||
height: 72px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.paper-att-card__preview {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card--image,
|
||||
.paper-att-card--video {
|
||||
.paper-att-card__preview {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card__type-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&--doc {
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&--video {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper-att-card__name {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.paper-att-card__actions {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.paper-att-card:hover .paper-att-card__actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.paper-att-card__action {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #047857;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user