This commit is contained in:
zlwu committed 2026-05-28 20:04:27 +08:00
1 parent 912ada02eb
commit 239d0a03d7
24 files changed
+935 -408

No files matched your search

@@ -160,6 +160,8 @@ export function fetchOutboundReleaseReviewList(params?: {
keyword?: string
/** pilot_service | clinical_application */
bizType?: string
infusionTimeStart?: string
infusionTimeEnd?: string
}) {
return request.get<PageList<OutboundReleaseReview>>({
url: '/web/admin/pilot-release-reviews',
@@ -185,31 +185,58 @@
v-for="(att, attIdx) in row.attachments"
:key="attIdx"
class="paper-att-card"
:class="{
'paper-att-card--image': isImageFile(att),
'paper-att-card--video': isVideoFile(att)
}"
:title="att.fileName"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<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="fullFileUrl(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)"
v-if="isImageFile(att) || isVideoFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
@click.stop="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
@click.stop="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
<el-icon
class="paper-att-card__action paper-att-card__action--danger"
title="删除"
@click="removePaperAttachment($index, attIdx)"
@click.stop="removePaperAttachment($index, attIdx)"
>
<Delete />
</el-icon>
@@ -357,7 +384,8 @@
Search,
Document,
Download,
View
View,
VideoPlay
} from '@element-plus/icons-vue'
import { fetchUploadAttachment } from '@/api/business'
import { fetchCellProductList } from '@/api/cell-product'
@@ -874,6 +902,11 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-upload) {
display: block;
line-height: normal;
}
}
.paper-att-card {
@@ -884,17 +917,62 @@
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
align-items: stretch;
justify-content: center;
padding: 6px 4px;
padding: 0;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
.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 {
@@ -944,6 +1022,10 @@
}
.paper-att-card--trigger {
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px 4px;
border: 1px dashed #a7f3d0;
background: transparent;
cursor: pointer;
@@ -961,14 +1043,20 @@
}
.paper-att-card__trigger-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #10b981;
margin-bottom: 2px;
margin: 0;
line-height: 1;
}
.paper-att-card--trigger span {
font-size: 11px;
color: #64748b;
line-height: 1.2;
text-align: center;
}
.preview-container {
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_storage_release_review_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1"}, {"id": "qc_work_order_index_formattempcellproductfield", "label": "formatTempCellProductField()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L840"}, {"id": "qc_work_order_index_handlereceptionattachmentpreview", "label": "handleReceptionAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1217"}, {"id": "qc_work_order_index_handlereceptionattachmentdownload", "label": "handleReceptionAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1222"}, {"id": "qc_work_order_index_taskattachmentfullurl", "label": "taskAttachmentFullUrl()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1226"}, {"id": "qc_work_order_index_handletaskattachmentpreview", "label": "handleTaskAttachmentPreview()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1233"}, {"id": "qc_work_order_index_handletaskattachmentdownload", "label": "handleTaskAttachmentDownload()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1238"}, {"id": "qc_work_order_index_formatconfiguredlabitems", "label": "formatConfiguredLabItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1254"}, {"id": "qc_work_order_index_formatconfiguredthirditems", "label": "formatConfiguredThirdItems()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1260"}, {"id": "qc_work_order_index_buildtasktypesections", "label": "buildTaskTypeSections()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1271"}, {"id": "qc_work_order_index_buildconfiguredonlysection", "label": "buildConfiguredOnlySection()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1360"}, {"id": "qc_work_order_index_formatdetectmethods", "label": "formatDetectMethods()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1378"}, {"id": "qc_work_order_index_formatmetric", "label": "formatMetric()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1385"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_formattempcellproductfield", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L840", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentpreview", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1217", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_handlereceptionattachmentdownload", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1222", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_work_order_index_vue", "target": "qc_work_order_index_taskattachmentfullurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue", "source_location": "L1226", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_vieLine truncated
@@ -10,21 +10,42 @@ import {
qcStatusLabel,
qcStatusTagType,
scheduleStatusLabel,
scheduleStatusTagType
scheduleStatusTagType,
sexLabel
} from '@/utils/status-tag-registry'
/** 出库质检 + 中试服务:申请单 / 患者 / 输注方案 / 输注计划 / 产品明细 */
export function buildOutboundPilotContextSections(detail: OutboundQcDetail): ViewSection[] {
export type OutboundViewContextVariant = 'pilot_service' | 'clinical_application'
const SECTION_TITLES: Record<
OutboundViewContextVariant,
{ application: string; schedule: string }
> = {
pilot_service: {
application: '中试服务申请单信息',
schedule: '本次输注计划信息'
},
clinical_application: {
application: '临床应用申请信息',
schedule: '临床应用输注计划信息'
}
}
/** 出库工单/质检:申请单 / 患者 / 输注方案 / 输注计划 / 产品明细(码值转中文标签) */
export function buildOutboundPilotContextSections(
detail: OutboundQcDetail,
variant: OutboundViewContextVariant = 'pilot_service'
): ViewSection[] {
const app = detail.application
const patient = detail.patient
const plan = detail.infusionPlan
const schedule = detail.infusionSchedule
const products = detail.products || []
const titles = SECTION_TITLES[variant]
const result: ViewSection[] = []
if (app) {
result.push({
title: '中试服务申请单信息',
title: titles.application,
fields: [
{ label: '申请单号', value: app.applicationNo || '—' },
{ label: '申请单名称', value: app.applicationName || '—' },
@@ -43,7 +64,7 @@ export function buildOutboundPilotContextSections(detail: OutboundQcDetail): Vie
title: '患者信息',
fields: [
{ label: '患者姓名', value: patient.patientName || '—' },
{ label: '性别', value: patient.sex || '—' },
{ label: '性别', value: sexLabel(String(patient.sex ?? '')) || '—' },
{ label: '年龄', value: patient.age || '—' },
{ label: '联系电话', value: patient.phoneNo || '—' },
{
@@ -98,7 +119,7 @@ export function buildOutboundPilotContextSections(detail: OutboundQcDetail): Vie
if (schedule.cancelReason) {
scheduleFields.push({ label: '取消原因', value: schedule.cancelReason, span: true })
}
result.push({ title: '本次输注计划信息', fields: scheduleFields })
result.push({ title: titles.schedule, fields: scheduleFields })
}
if (products.length) {
@@ -212,31 +212,58 @@
v-for="(att, attIdx) in row.attachments"
:key="attIdx"
class="paper-att-card"
:class="{
'paper-att-card--image': isImageFile(att),
'paper-att-card--video': isVideoFile(att)
}"
:title="att.fileName"
>
<el-icon class="paper-att-card__icon"><Document /></el-icon>
<span class="paper-att-card__name" :title="att.fileName">{{
att.fileName || '附件'
}}</span>
<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="fullFileUrl(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)"
v-if="isImageFile(att) || isVideoFile(att)"
class="paper-att-card__action"
title="预览"
@click="handlePreviewPaperAttachment(att)"
@click.stop="handlePreviewPaperAttachment(att)"
>
<View />
</el-icon>
<el-icon
class="paper-att-card__action"
title="下载"
@click="handleDownloadPaperAttachment(att)"
@click.stop="handleDownloadPaperAttachment(att)"
>
<Download />
</el-icon>
<el-icon
class="paper-att-card__action paper-att-card__action--danger"
title="删除"
@click="removePaperAttachment($index, attIdx)"
@click.stop="removePaperAttachment($index, attIdx)"
>
<Delete />
</el-icon>
@@ -398,7 +425,8 @@
Search,
Document,
Download,
View
View,
VideoPlay
} from '@element-plus/icons-vue'
import { fetchUploadAttachment } from '@/api/business'
import { fetchCellProductList } from '@/api/cell-product'
@@ -908,6 +936,11 @@
display: flex;
flex-wrap: wrap;
gap: 8px;
:deep(.el-upload) {
display: block;
line-height: normal;
}
}
.paper-att-card {
@@ -918,17 +951,62 @@
background: #f8fafc;
display: flex;
flex-direction: column;
align-items: center;
align-items: stretch;
justify-content: center;
padding: 6px 4px;
padding: 0;
position: relative;
overflow: hidden;
}
.paper-att-card__icon {
color: #0284c7;
font-size: 18px;
.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 {
@@ -978,6 +1056,10 @@
}
.paper-att-card--trigger {
align-items: center;
justify-content: center;
gap: 4px;
padding: 6px 4px;
border: 1px dashed #a7f3d0;
background: transparent;
cursor: pointer;
@@ -995,14 +1077,20 @@
}
.paper-att-card__trigger-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #10b981;
margin-bottom: 2px;
margin: 0;
line-height: 1;
}
.paper-att-card--trigger span {
font-size: 11px;
color: #64748b;
line-height: 1.2;
text-align: center;
}
.preview-container {
@@ -33,9 +33,22 @@
/>
</el-select>
</el-form-item>
<el-form-item label="输注时间">
<el-date-picker
v-model="searchForm.infusionTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按申请单号、患者姓名搜索。</span>
<span class="filter-hint"
>支持按申请单号、患者姓名搜索;输注时间默认今天前 15 天至后 15 天。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -267,9 +280,27 @@
{ value: 'btg', label: '不通过' }
]
function formatLocalDate(d: Date) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
/** 输注时间默认:今天前 15 天 ~ 今天后 15 天 */
function getDefaultInfusionTimeRange(): [string, string] {
const today = new Date()
const start = new Date(today)
start.setDate(start.getDate() - 15)
const end = new Date(today)
end.setDate(end.getDate() + 15)
return [formatLocalDate(start), formatLocalDate(end)]
}
const searchForm = reactive({
reviewConclusion: '',
keyword: ''
keyword: '',
infusionTimeRange: getDefaultInfusionTimeRange() as [string, string]
})
const tableData = ref<OutboundReleaseReview[]>([])
const selectedRows = ref<OutboundReleaseReview[]>([])
@@ -290,7 +321,9 @@
pageSize: pagination.pageSize,
reviewConclusion: searchForm.reviewConclusion || undefined,
keyword: searchForm.keyword || undefined,
bizType: effectiveBizType.value
bizType: effectiveBizType.value,
infusionTimeStart: searchForm.infusionTimeRange?.[0] || undefined,
infusionTimeEnd: searchForm.infusionTimeRange?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -307,6 +340,7 @@
function handleReset() {
searchForm.reviewConclusion = ''
searchForm.keyword = ''
searchForm.infusionTimeRange = getDefaultInfusionTimeRange()
pagination.page = 1
loadData()
}
@@ -199,10 +199,10 @@
>
<template #default="{ row }">
{{
row.amount
row.amount != null && row.amount !== ''
? '¥' +
Number(row.amount).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
: '-'
: '—'
}}
</template>
</el-table-column>
@@ -215,13 +215,13 @@
>
<template #default="{ row }">
<el-tag
v-if="row.paymentStatus"
:type="payStatusTagType(row.paymentStatus)"
v-if="row.payStatus"
:type="payStatusTagType(row.payStatus)"
size="small"
>
{{ payStatusLabel(row.paymentStatus) }}
{{ payStatusLabel(row.payStatus) }}
</el-tag>
<span v-else>-</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
@@ -552,10 +552,21 @@
}
const buildCompleteConfirmMessage = (row: any) => {
const orderTypeLabel = channelServiceOrderTypeLabel(row.orderType)
const patient = row.patientName ? `患者「${row.patientName}」` : '该患者'
const pendingConfirmHint =
getInfusionStatus(row) === 'dtz' ? '当前为待输注,系统将先记录用户确认再标记完成。' : ''
if (isClinical.value) {
const paymentHint =
'临床应用不在此扣减渠道服务订单费用,缴费请在「缴费信息」中单独处理。'
return `${patient}的输注计划将标记为已完成。${pendingConfirmHint}${paymentHint}是否继续?`
}
const orderTypeLabel = channelServiceOrderTypeLabel(row.orderType)
const statusHint =
getInfusionStatus(row) === 'dtz' ? '当前为待输注,系统将先记录用户确认再完成结算。' : ''
getInfusionStatus(row) === 'dtz'
? '当前为待输注,系统将先记录用户确认再完成结算。'
: ''
let settlementHint = '并触发关联服务订单的结算扣减。'
if (row.orderType === 'yfk') {
settlementHint = `关联服务订单类型为「${orderTypeLabel}」,系统将按渠道协议单价扣减服务订单账户余额。`
@@ -603,7 +614,9 @@
scheduleId,
infusionStatus: 'ywc'
})
ElMessage.success('输注计划已完成,结算扣减已处理')
ElMessage.success(
isClinical.value ? '输注计划已完成' : '输注计划已完成,结算扣减已处理'
)
loadData()
} catch (e: any) {
if (e !== 'cancel') {
@@ -14,8 +14,8 @@
</el-tag>
<el-button
v-if="editor.selectedStep.value.stepStatus === 'dzx'"
class="btn-op-item primary"
size="small"
class="btn-main task-exec-toolbar-btn"
type="primary"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@click="editor.handleStartStep(editor.selectedStep.value)"
@@ -35,9 +35,8 @@
</el-button>
</template>
<el-button
class="btn-main"
class="btn-main task-exec-toolbar-btn"
type="primary"
size="small"
:loading="editor.manualSaveLoading.value"
:disabled="editor.stepActionLoading.value"
@click="editor.handleManualSave"
@@ -40,9 +40,8 @@
<div v-if="!readonly" class="execution-header-actions">
<el-button
v-if="editor.taskDetail.value.taskStatus === 'dpz'"
class="btn-main"
class="btn-main task-exec-toolbar-btn"
type="primary"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleStartTask"
>
@@ -53,9 +52,8 @@
editor.taskDetail.value.taskStatus === 'jxz' ||
editor.taskDetail.value.taskStatus === 'ywc'
"
class="btn-main"
class="btn-main task-exec-toolbar-btn"
type="success"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleCompleteTask"
>
@@ -263,13 +261,13 @@
<h3>📥 原材料(样本/细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
class="btn-main task-exec-toolbar-btn"
type="primary"
size="small"
:loading="editor.inputSaving.value"
@click="editor.saveTaskInputs"
><el-icon><Check /></el-icon>保存</el-button
>
<el-icon><Check /></el-icon>保存
</el-button>
</div>
</div>
<div class="execution-detail-body">
@@ -293,13 +291,13 @@
<h3>📤 产出(细胞产品)</h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
class="btn-main task-exec-toolbar-btn"
type="primary"
size="small"
:loading="editor.outputSaving.value"
@click="editor.saveTaskOutputs"
><el-icon><Check /></el-icon>保存</el-button
>
<el-icon><Check /></el-icon>保存
</el-button>
</div>
</div>
<div class="execution-detail-body">
@@ -712,3 +710,26 @@
margin-right: 8px;
}
</style>
<!-- 弹窗 teleport 到 body,工具栏按钮样式需挂 execution-dialog 全局类 -->
<style lang="scss">
.execution-dialog {
.task-exec-toolbar-btn.el-button {
height: 36px;
min-height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
> span {
font-size: 14px;
gap: 6px;
}
.el-icon {
font-size: 14px;
}
}
}
</style>
@@ -4,11 +4,11 @@
<div class="panel-top">
<h2 class="title-bar-heading">原材料(样本/细胞产品)</h2>
<div v-if="!disabled" class="panel-top-actions">
<el-button type="primary" size="small" @click="handleAddSample">
<el-button class="btn-main task-exec-toolbar-btn" type="primary" @click="handleAddSample">
<el-icon><Plus /></el-icon>
添加样本
</el-button>
<el-button size="small" @click="handleAddCellProduct">
<el-button class="btn-ghost task-exec-toolbar-btn" @click="handleAddCellProduct">
<el-icon><Plus /></el-icon>
添加细胞产品
</el-button>
@@ -3,7 +3,12 @@
<!-- 工具栏 -->
<div class="panel-top">
<h2 class="title-bar-heading">产出(细胞产品)</h2>
<el-button v-if="!disabled" type="primary" size="small" @click="handleAdd">
<el-button
v-if="!disabled"
class="btn-main task-exec-toolbar-btn"
type="primary"
@click="handleAdd"
>
<el-icon><Plus /></el-icon>
新增产出
</el-button>
@@ -1,72 +1,46 @@
<!-- 生产工单展开行 · 生产任务子列表 -->
<template>
<div class="work-order-task-sublist">
<div class="list-toolbar work-order-task-sublist__toolbar">
<div class="list-toolbar-row">
<el-button
class="btn-main"
type="primary"
size="small"
:disabled="!canDispatch"
@click="handleDispatch"
<div class="work-order-task-sublist__panel" v-loading="loading">
<div v-if="tableData.length" class="work-order-task-cards-wrap">
<div class="work-order-task-sublist__summary">
<span class="work-order-task-sublist__count">共 {{ tableData.length }} 项任务</span>
<el-button
class="work-order-task-sublist__refresh-icon"
link
type="primary"
:loading="loading"
@click="loadData"
>
<el-icon><RefreshLeft /></el-icon>
</el-button>
<el-button
class="btn-main work-order-task-sublist__dispatch"
type="primary"
size="small"
:disabled="!canDispatch"
@click="handleDispatch"
>
<el-icon><Position /></el-icon>派发任务
</el-button>
</div>
<div
v-for="(row, idx) in tableData"
:key="row.id || idx"
class="work-order-task-card"
@dblclick="handleViewDetail(row)"
>
<el-icon><Position /></el-icon>派发任务
</el-button>
<el-button class="btn-ghost" size="small" :loading="loading" @click="loadData">
<el-icon><RefreshLeft /></el-icon>刷新
</el-button>
</div>
</div>
<div class="work-order-task-sublist__body">
<el-table
class="list-table work-order-task-sublist__table"
:data="tableData"
v-loading="loading"
stripe
size="small"
@row-dblclick="handleViewDetail"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i) => i + 1"
/>
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="taskType" label="任务类型" min-width="120" align="center">
<template #default="{ row }">
<div class="work-order-task-card__head">
<span class="work-order-task-card__name">{{ row.taskName || row.taskNo || '—' }}</span>
<el-tag :type="taskStatusTag(row.taskStatus).type" size="small">
{{ taskStatusTag(row.taskStatus).label }}
</el-tag>
</div>
<div class="work-order-task-card__meta">
<span class="work-order-task-card__code">{{ row.taskNo || '—' }}</span>
<el-tag :type="productionTaskTypeTag(row.taskType, row.orderSource).type" size="small">
{{ productionTaskTypeTag(row.taskType, row.orderSource).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="taskName"
label="任务名称"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="routeName"
label="工艺路线"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.routeName || '—' }}</template>
</el-table-column>
<el-table-column prop="taskStatus" label="任务状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="taskStatusTag(row.taskStatus).type" size="small">{{
taskStatusTag(row.taskStatus).label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="conclusion" label="生产结论" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="productionConclusionTagType(row.conclusion)"
:type="productionConclusionTagType(row.conclusion)"
@@ -74,63 +48,81 @@
>
{{ row.conclusionLabel || productionConclusionLabel(row.conclusion) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column prop="startedTime" label="开始时间" min-width="170" align="center">
<template #default="{ row }">{{ row.startedTime || '—' }}</template>
</el-table-column>
<el-table-column prop="completedTime" label="完成时间" min-width="170" align="center">
<template #default="{ row }">{{ row.completedTime || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<div class="work-order-task-sublist__ops">
<el-button
v-if="canEnterExecution(row)"
class="btn-op-item primary"
type="primary"
@click="handleEnterExecution(row)"
>
<el-icon><VideoPlay /></el-icon>进入执行
</div>
<div class="work-order-task-sublist__ops">
<el-button
v-if="canEnterExecution(row)"
class="btn-op-item primary"
type="primary"
size="small"
@click.stop="handleEnterExecution(row)"
>
<el-icon><VideoPlay /></el-icon>进入执行
</el-button>
<el-button
v-else
class="btn-op-item primary"
type="primary"
size="small"
@click.stop="handleViewDetail(row)"
>
<el-icon><View /></el-icon>查看
</el-button>
<el-dropdown
v-if="hasMoreTaskActions(row)"
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item" size="small" @click.stop>
<el-icon><More /></el-icon>
</el-button>
<el-button v-else class="btn-op-item primary" @click="handleViewDetail(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-dropdown
v-if="hasMoreTaskActions(row)"
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item">
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canEnterExecution(row)" command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item v-if="canEditTask(row)" command="editInfo">
<el-icon><EditPen /></el-icon>编辑信息
</el-dropdown-item>
<el-dropdown-item v-if="canEditTask(row)" command="editRoute">
<el-icon><EditPen /></el-icon>编辑工艺
</el-dropdown-item>
<el-dropdown-item v-if="canDeleteTask(row)" command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="!loading && !tableData.length"
description="暂无任务,可点击「派发任务」创建"
:image-size="72"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canEnterExecution(row)" command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item v-if="canEditTask(row)" command="editInfo">
<el-icon><EditPen /></el-icon>编辑信息
</el-dropdown-item>
<el-dropdown-item v-if="canEditTask(row)" command="editRoute">
<el-icon><EditPen /></el-icon>编辑工艺
</el-dropdown-item>
<el-dropdown-item v-if="canDeleteTask(row)" command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div v-else-if="!loading" class="work-order-task-cards-wrap work-order-task-cards-wrap--empty">
<div class="work-order-task-sublist__summary">
<el-button
class="work-order-task-sublist__refresh-icon"
link
type="primary"
:loading="loading"
@click="loadData"
>
<el-icon><RefreshLeft /></el-icon>
</el-button>
<el-button
class="btn-main work-order-task-sublist__dispatch"
type="primary"
size="small"
:disabled="!canDispatch"
@click="handleDispatch"
>
<el-icon><Position /></el-icon>派发任务
</el-button>
</div>
<el-empty
class="work-order-task-sublist__empty"
description="暂无任务,可点击「派发任务」创建"
:image-size="56"
/>
</div>
</div>
<TaskExecutionDialog
@@ -325,36 +317,139 @@
<style scoped lang="scss">
.work-order-task-sublist {
padding: 12px 16px 16px 40px;
padding: 8px 12px 10px 40px;
background: #f8fafc;
text-align: left;
}
.work-order-task-sublist__toolbar {
padding: 0 0 8px;
border-bottom: none;
.work-order-task-sublist__panel {
min-height: 32px;
}
.work-order-task-sublist__body {
display: block;
width: 100%;
.work-order-task-cards-wrap {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.work-order-task-sublist__summary {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 8px;
padding-top: 8px;
min-height: 34px;
}
.work-order-task-sublist__count {
font-size: 12px;
color: #94a3b8;
line-height: 1.35;
white-space: nowrap;
}
.work-order-task-sublist__refresh-icon {
flex-shrink: 0;
padding: 0 2px;
height: 20px;
font-size: 14px;
.el-icon {
font-size: 14px;
}
}
.work-order-task-sublist__dispatch {
flex-shrink: 0;
}
.work-order-task-cards-wrap--empty {
align-items: center;
.work-order-task-sublist__summary {
padding-top: 0;
min-height: auto;
}
}
.work-order-task-sublist__empty {
flex: 1;
min-width: 200px;
padding: 4px 0;
:deep(.el-empty__description) {
margin-top: 4px;
}
}
.work-order-task-card {
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 248px;
width: 248px;
max-width: 248px;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
cursor: default;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
&:hover {
border-color: #a7f3d0;
box-shadow: 0 2px 6px rgba(16, 185, 129, 0.1);
}
}
.work-order-task-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
min-width: 0;
}
.work-order-task-card__name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 600;
color: #1e293b;
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
}
.work-order-task-card__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 6px;
min-width: 0;
}
.work-order-task-card__code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
color: #475569;
line-height: 1.35;
max-width: 100%;
overflow: hidden;
}
.work-order-task-sublist__table {
width: 100%;
:deep(.el-table__inner-wrapper),
:deep(.el-table__header-wrapper),
:deep(.el-table__body-wrapper) {
width: 100%;
}
text-overflow: ellipsis;
white-space: nowrap;
}
.work-order-task-sublist__ops {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding-top: 2px;
}
</style>
@@ -141,9 +141,14 @@
align="center"
>
<template #default="{ row }">
<el-tag :type="row.bizType === 'pilot_service' ? 'primary' : 'success'" size="small">
{{ row.bizTypeLabel || '—' }}
<el-tag
v-if="row.bizType"
:type="bizTypeTagType(row.bizType) as TagType"
size="small"
>
{{ row.bizTypeLabel || bizTypeLabel(row.bizType) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
@@ -546,7 +551,9 @@
donationStatusLabel,
donationStatusTagType,
productStatusLabel,
productStatusTagType
productStatusTagType,
bizTypeLabel,
bizTypeTagType
} from '@/utils/status-tag-registry'
import { buildOutboundPilotContextSections } from '@/utils/outbound-qc-pilot-view-sections'
import type { OutboundQcDetail } from '@/api/qc-work-order'
@@ -873,15 +880,17 @@
const viewVisible = ref(false)
const detail = ref<ProductionWorkOrderDetail | null>(null)
const isOutboundPilotService = computed(
const isOutboundContextView = computed(
() =>
detail.value?.workOrderType === 'outbound' && detail.value?.bizType === 'pilot_service'
detail.value?.workOrderType === 'outbound' &&
(detail.value?.bizType === 'pilot_service' ||
detail.value?.bizType === 'clinical_application')
)
const viewDialogWidth = computed(() => (isOutboundPilotService.value ? '1200px' : '720px'))
const viewDialogWidth = computed(() => (isOutboundContextView.value ? '1200px' : '720px'))
const viewDialogGridColumns = computed<2 | 3 | 4>(() =>
isOutboundPilotService.value ? 4 : 2
isOutboundContextView.value ? 4 : 2
)
const viewDialogCompact = computed(() => isOutboundPilotService.value)
const viewDialogCompact = computed(() => isOutboundContextView.value)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewReceptionAttachmentFiles = computed(() => {
@@ -959,14 +968,8 @@
if (isOutbound) {
baseFields.push({
label: '业务类型',
value:
d.bizTypeLabel ||
(d.bizType === 'pilot_service'
? '中试服务'
: d.bizType === 'clinical_application'
? '临床应用'
: '—'),
tagType: 'primary' as const
value: d.bizTypeLabel || bizTypeLabel(d.bizType || '') || '—',
tagType: bizTypeTagType(d.bizType || '') as TagType
})
}
@@ -1010,62 +1013,19 @@
const sections: ViewSection[] = [{ title: '基本信息', fields: baseFields }]
// outbound: 中试服务 — 与出库质检工单查看弹窗一致;临床应用 — 保留简要申请/输注信息
if (isOutbound) {
if (d.bizType === 'pilot_service') {
const pilotDetail: OutboundQcDetail = {
workOrder: {} as OutboundQcDetail['workOrder'],
application: d.application,
patient: d.patient,
infusionPlan: d.infusionPlan,
infusionSchedule: d.infusionSchedule,
products: d.products
}
sections.push(...buildOutboundPilotContextSections(pilotDetail))
} else {
const pa = d.pilotApplication || {}
const sched = d.infusionSchedule || {}
const appStatusLabel = (s: string) => {
const m: Record<string, string> = {
dks: '待开始',
jxz: '进行中',
ywc: '已完成',
yzf: '已作废'
}
return m[s] || s || '—'
}
const appStatusTagType = (s: string) => {
const m: Record<string, string> = {
dks: 'info',
jxz: 'warning',
ywc: 'success',
yzf: 'danger'
}
return m[s] || 'info'
}
sections.push({
title: '中试申请信息',
fields: [
{ label: '申请单号', value: pa.applicationNo || '—' },
{ label: '申请名称', value: pa.applicationName || '—' },
{
label: '申请状态',
value: appStatusLabel(pa.applicationStatus || ''),
tagType: appStatusTagType(pa.applicationStatus || '') as any
}
]
})
sections.push({
title: '输注计划信息',
fields: [
{ label: '细胞类型', value: sched.cellType || '—' },
{ label: '规格', value: sched.specText || '—' },
{ label: '输注时间', value: sched.infusionTime || '—' },
{ label: '输注地点', value: sched.infusionLocation || '—' },
{ label: '输注方式', value: sched.infusionMethod || '—' }
]
})
// outbound:中试服务 / 临床应用 — 与出库质检一致,码值展示中文标签
if (isOutbound && (d.bizType === 'pilot_service' || d.bizType === 'clinical_application')) {
const outboundDetail: OutboundQcDetail = {
workOrder: {} as OutboundQcDetail['workOrder'],
application: d.application,
patient: d.patient,
infusionPlan: d.infusionPlan,
infusionSchedule: d.infusionSchedule,
products: d.products
}
const variant =
d.bizType === 'clinical_application' ? 'clinical_application' : 'pilot_service'
sections.push(...buildOutboundPilotContextSections(outboundDetail, variant))
return sections
}
@@ -20,7 +20,13 @@
/>
</template>
<template #product-table>
<el-table :data="detail?.products || []" stripe size="small" class="outbound-product-table">
<div v-if="detail?.products?.length" class="view-subtable-wrap">
<el-table
:data="detail?.products || []"
stripe
size="small"
class="list-table view-subtable outbound-product-table"
>
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="projectCode" label="项目编码" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" min-width="120" align="center" show-overflow-tooltip />
@@ -35,6 +41,8 @@
</template>
</el-table-column>
</el-table>
</div>
<p v-else class="view-empty-hint">暂无产品明细</p>
</template>
<template v-for="task in tasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
<div :key="task.id" class="outbound-qc-task-attachments">
@@ -469,6 +477,24 @@
}
.outbound-product-table {
margin-top: 4px;
margin-top: 0;
:deep(.el-table__header th.el-table__cell),
:deep(.el-table__body td.el-table__cell) {
font-size: 12px;
padding: 6px 0;
}
:deep(.el-table__header th.el-table__cell .cell),
:deep(.el-table__body td.el-table__cell .cell) {
font-size: 12px;
line-height: 1.35;
}
:deep(.el-tag) {
--el-tag-font-size: 11px;
height: 20px;
padding: 0 6px;
}
}
</style>
@@ -2,131 +2,116 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="qc-work-order-task-sublist">
<div class="list-toolbar qc-work-order-task-sublist__toolbar">
<div class="list-toolbar-row">
<el-button class="btn-ghost" size="small" :loading="loading" @click="loadData">
<el-icon><RefreshLeft /></el-icon>刷新
</el-button>
</div>
</div>
<div class="qc-work-order-task-sublist__body sqxp-subtable-wrap">
<el-table
class="list-table qc-work-order-task-sublist__table sqxp-subtable"
:data="tableData"
v-loading="loading"
stripe
size="small"
@row-dblclick="handleView"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i) => i + 1"
/>
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="taskType" label="检测类型" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.taskType === 'sysz' ? 'success' : 'info'" size="small">{{
taskTypeLabel(row.taskType)
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="itemName" label="检测项目" min-width="160" align="center" />
<el-table-column prop="companyName" label="第三方公司" min-width="120" align="center">
<template #default="{ row }">{{ row.companyName || '—' }}</template>
</el-table-column>
<el-table-column prop="conclusion" label="质检结论" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.conclusion === 'tg'" type="success" size="small">
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag v-else-if="row.conclusion === 'btg'" type="danger" size="small">
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag v-else type="info" size="small">—</el-tag>
</template>
</el-table-column>
<el-table-column prop="taskStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag
:type="
row.taskStatus === 'ywc'
? 'success'
: row.taskStatus === 'jxz'
? 'warning'
: 'info'
"
size="small"
>
{{ taskStatusLabel(row.taskStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
show-overflow-tooltip
<div class="qc-work-order-task-sublist__panel" v-loading="loading">
<div v-if="tableData.length" class="qc-task-cards-wrap">
<div class="qc-work-order-task-sublist__summary">
<span class="qc-work-order-task-sublist__count">共 {{ tableData.length }} 项任务</span>
<el-button
class="qc-work-order-task-sublist__refresh-icon"
link
type="primary"
:loading="loading"
@click="loadData"
>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<div class="qc-work-order-task-sublist__ops">
<template v-if="hasPrimaryTaskActions(row)">
<el-button
v-if="canStartTask(row)"
class="btn-op-item primary"
type="primary"
@click="handleStart(row)"
>
<el-icon><VideoPlay /></el-icon>开始
</el-button>
<el-button
v-if="canFillResult(row)"
class="btn-op-item primary"
type="primary"
@click="openResult(row)"
>
<el-icon><EditPen /></el-icon>填写结果
</el-button>
</template>
<el-button v-else class="btn-op-item primary" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-dropdown
v-if="hasMoreTaskActions(row)"
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item">
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="hasPrimaryTaskActions(row)" command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item v-if="canDeleteTask(row)" command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-icon><RefreshLeft /></el-icon>
</el-button>
</div>
<div
v-for="(row, idx) in tableData"
:key="row.id || idx"
class="qc-task-card"
@dblclick="handleView(row)"
>
<div class="qc-task-card__head">
<span class="qc-task-card__name">{{ row.itemName || row.taskNo || '—' }}</span>
<el-tag :type="taskStatusTagType(row.taskStatus)" size="small">
{{ taskStatusLabel(row.taskStatus) }}
</el-tag>
</div>
<div class="qc-task-card__meta">
<span class="qc-task-card__code">{{ row.taskNo || '—' }}</span>
<el-tag :type="taskTypeTagType(row.taskType)" size="small">
{{ taskTypeLabel(row.taskType) }}
</el-tag>
<el-tag
v-if="row.conclusion === 'tg' || row.conclusion === 'btg'"
:type="conclusionTagType(row.conclusion)"
size="small"
>
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
</div>
<div class="qc-work-order-task-sublist__ops">
<template v-if="hasPrimaryTaskActions(row)">
<el-button
v-if="canStartTask(row)"
class="btn-op-item primary"
type="primary"
size="small"
@click.stop="handleStart(row)"
>
<el-icon><VideoPlay /></el-icon>开始
</el-button>
<el-button
v-if="canFillResult(row)"
class="btn-op-item primary"
type="primary"
size="small"
@click.stop="openResult(row)"
>
<el-icon><EditPen /></el-icon>填写结果
</el-button>
</template>
</el-table-column>
</el-table>
<el-button
v-else
class="btn-op-item primary"
type="primary"
size="small"
@click.stop="handleView(row)"
>
<el-icon><View /></el-icon>查看
</el-button>
<el-dropdown
v-if="hasMoreTaskActions(row)"
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item" size="small" @click.stop>
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="hasPrimaryTaskActions(row)" command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item v-if="canDeleteTask(row)" command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div v-else-if="!loading" class="qc-task-cards-wrap qc-task-cards-wrap--empty">
<div class="qc-work-order-task-sublist__summary">
<el-button
class="qc-work-order-task-sublist__refresh-icon"
link
type="primary"
:loading="loading"
@click="loadData"
>
<el-icon><RefreshLeft /></el-icon>
</el-button>
</div>
<el-empty
v-if="!loading && !tableData.length"
class="qc-work-order-task-sublist__empty"
description="暂无任务,请先在工单行「配置检测」"
:image-size="72"
:image-size="56"
/>
</div>
</div>
<el-dialog
v-model="resultVisible"
@@ -607,6 +592,22 @@
}
}
function taskStatusTagType(status: string) {
if (status === 'ywc') return 'success' as const
if (status === 'jxz') return 'warning' as const
return 'info' as const
}
function taskTypeTagType(type: string) {
return type === 'sysz' ? ('success' as const) : ('info' as const)
}
function conclusionTagType(conclusion: string) {
if (conclusion === 'tg') return 'success' as const
if (conclusion === 'btg') return 'danger' as const
return 'info' as const
}
const resultSubmitLabel = computed(() =>
resultTaskStatus.value === 'ywc' ? '保存' : '提交并完成'
)
@@ -1212,26 +1213,135 @@
<style scoped lang="scss">
.qc-work-order-task-sublist {
padding: 12px 16px 16px 40px;
padding: 8px 12px 10px 40px;
background: #f8fafc;
text-align: left;
}
.qc-work-order-task-sublist__toolbar {
padding: 0 0 8px;
border-bottom: none;
.qc-work-order-task-sublist__panel {
min-height: 32px;
}
.qc-work-order-task-sublist__body {
display: block;
width: 100%;
.qc-task-cards-wrap {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.qc-work-order-task-sublist__summary {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 2px;
padding-top: 8px;
min-height: 34px;
}
.qc-work-order-task-sublist__count {
font-size: 12px;
color: #94a3b8;
line-height: 1.35;
white-space: nowrap;
}
.qc-work-order-task-sublist__refresh-icon {
flex-shrink: 0;
padding: 0 2px;
height: 20px;
font-size: 14px;
.el-icon {
font-size: 14px;
}
}
.qc-task-cards-wrap--empty {
align-items: center;
.qc-work-order-task-sublist__summary {
padding-top: 0;
min-height: auto;
}
}
.qc-work-order-task-sublist__empty {
flex: 1;
min-width: 200px;
padding: 4px 0;
:deep(.el-empty__description) {
margin-top: 4px;
}
}
.qc-task-card {
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 248px;
width: 248px;
max-width: 248px;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
cursor: default;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
&:hover {
border-color: #a7f3d0;
box-shadow: 0 2px 6px rgba(16, 185, 129, 0.1);
}
}
.qc-task-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
min-width: 0;
}
.qc-task-card__name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 600;
color: #1e293b;
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
}
.qc-task-card__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 6px;
min-width: 0;
}
.qc-task-card__code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
color: #475569;
line-height: 1.35;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.qc-work-order-task-sublist__ops {
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding-top: 2px;
}
.form-section {