This commit is contained in:
zlwu committed 2026-06-16 18:33:52 +08:00
1 parent 8ff159c251
commit c1832f9d03
60 files changed
+13400 -10821

No files matched your search

@@ -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>