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

No files matched your search

+1 -1
View File
@@ -1 +1 @@
1098
69671
@@ -45,6 +45,8 @@ async def list_pilot_release_reviews(
scheduleStatus: Optional[str] = None,
qcStatus: Optional[str] = None,
bizType: Optional[str] = None,
infusionTimeStart: Optional[str] = None,
infusionTimeEnd: Optional[str] = None,
db: Session = Depends(get_db),
user: dict = Depends(require_menu_code("biz:outboundReleaseReview:list")),
):
@@ -58,6 +60,8 @@ async def list_pilot_release_reviews(
schedule_status=scheduleStatus,
qc_status=qcStatus,
biz_type=bizType,
infusion_time_start=infusionTimeStart,
infusion_time_end=infusionTimeEnd,
page=page,
page_size=pageSize,
user_info=user,
+22
View File
@@ -128,6 +128,28 @@ def map_payments_by_schedule_id(
return {row.infusion_schedule_id: _enrich_payment(db, row) for row in rows}
def map_payments_by_schedule_ids(
db: Session,
*,
schedule_ids: List[str],
tenant_no: str,
) -> Dict[str, Dict[str, Any]]:
"""按输注计划 ID 批量索引缴费记录(列表页展示用,不做关联 enrich)"""
unique_ids = [sid for sid in {sid for sid in schedule_ids if sid}]
if not unique_ids:
return {}
rows = (
db.query(BizPilotPayment)
.filter(
BizPilotPayment.infusion_schedule_id.in_(unique_ids),
BizPilotPayment.tenant_no == tenant_no,
BizPilotPayment.del_flag == 0,
)
.all()
)
return {row.infusion_schedule_id: _row_to_dict(row) for row in rows}
# ─── create (auto) ────────────────────────────────────────
+21 -1
View File
@@ -5,7 +5,7 @@ import math
from datetime import datetime
from typing import Any, Dict, List, Optional
from sqlalchemy import func, or_
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
@@ -118,6 +118,8 @@ def list_reviews_paginated(
schedule_status: Optional[str] = None,
qc_status: Optional[str] = None,
biz_type: Optional[str] = None,
infusion_time_start: Optional[str] = None,
infusion_time_end: Optional[str] = None,
page: int = 1,
page_size: int = 10,
user_info=None,
@@ -212,6 +214,24 @@ def list_reviews_paginated(
if qs:
q0 = q0.filter(BizPilotInfusionSchedule.qc_status == qs)
# ── 输注时间范围 ──
time_bounds = []
if (infusion_time_start or "").strip():
try:
start_dt = datetime.strptime(infusion_time_start.strip()[:10], "%Y-%m-%d")
time_bounds.append(BizPilotInfusionSchedule.infusion_time >= start_dt)
except ValueError:
pass
if (infusion_time_end or "").strip():
try:
end_dt = datetime.strptime(infusion_time_end.strip()[:10], "%Y-%m-%d")
end_dt = end_dt.replace(hour=23, minute=59, second=59)
time_bounds.append(BizPilotInfusionSchedule.infusion_time <= end_dt)
except ValueError:
pass
if time_bounds:
q0 = q0.filter(or_(and_(*time_bounds), BizPilotInfusionSchedule.infusion_time.is_(None)))
# ── review_conclusion 过滤 ──
rc = (review_conclusion or "").strip()
if rc == "pending":
@@ -385,6 +385,21 @@ def list_infusion_schedules_mgmt(
)
)
schedule_ids = [item["id"] for item in items if item.get("id")]
if schedule_ids:
from app.crud import clinical_payment
payment_map = clinical_payment.map_payments_by_schedule_ids(
db, schedule_ids=schedule_ids, tenant_no=tenant_no
)
for item in items:
payment = payment_map.get(item["id"])
if not payment:
continue
item["payment"] = payment
item["amount"] = payment.get("amount")
item["payStatus"] = payment.get("payStatus")
return {
"list": items,
"pagination": {"page": page, "pageSize": page_size, "total": total},
@@ -12,6 +12,8 @@ class OutboundReviewListParams(BaseModel):
reviewConclusion: Optional[str] = None
scheduleStatus: Optional[str] = None
qcStatus: Optional[str] = None
infusionTimeStart: Optional[str] = None
infusionTimeEnd: Optional[str] = None
class OutboundReviewSubmitBody(BaseModel):
+1 -1
View File
@@ -1,7 +1,7 @@
# Graph Report - src (2026-05-28)
## Corpus Check
- Large corpus: 672 files · ~417,392 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
- Large corpus: 672 files · ~417,381 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
## Summary
- 1934 nodes · 2494 edges · 50 communities detected
+2 -2
View File
@@ -329,7 +329,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779864070.4019275,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/OutputTypeTab.vue": 1779864078.964701,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1779525781.6949234,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779965464.3573413,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779965568.1579607,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue": 1779948870.746249,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue": 1779889633.846836,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779857055.2939715,
@@ -339,7 +339,7 @@
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/QcMaterialUsageTab.vue": 1779928630.307365,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779884110.7197206,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779926469.2154074,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue": 1779857044.9472554,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue": 1779966532.6566818,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue": 1779701230.688804,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue": 1779790362.6831455,
"/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779864358.234948,
@@ -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,10 +1,21 @@
<!-- 生产工单展开行 · 生产任务子列表 -->
<template>
<div class="work-order-task-sublist">
<div class="list-toolbar work-order-task-sublist__toolbar">
<div class="list-toolbar-row">
<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="btn-main"
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"
@@ -12,61 +23,24 @@
>
<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"
<div
v-for="(row, idx) in tableData"
:key="row.id || idx"
class="work-order-task-card"
@dblclick="handleViewDetail(row)"
>
<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,27 +48,24 @@
>
{{ 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>
<div class="work-order-task-sublist__ops">
<el-button
v-if="canEnterExecution(row)"
class="btn-op-item primary"
type="primary"
@click="handleEnterExecution(row)"
size="small"
@click.stop="handleEnterExecution(row)"
>
<el-icon><VideoPlay /></el-icon>进入执行
</el-button>
<el-button v-else class="btn-op-item primary" @click="handleViewDetail(row)">
<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
@@ -102,7 +73,7 @@
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item">
<el-button class="btn-op-item" size="small" @click.stop>
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
@@ -123,15 +94,36 @@
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
</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
v-if="!loading && !tableData.length"
class="work-order-task-sublist__empty"
description="暂无任务,可点击「派发任务」创建"
:image-size="72"
:image-size="56"
/>
</div>
</div>
<TaskExecutionDialog
v-model:visible="executionDialogVisible"
@@ -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,10 +1013,9 @@
const sections: ViewSection[] = [{ title: '基本信息', fields: baseFields }]
// outbound: 中试服务 — 与出库质检工单查看弹窗一致;临床应用 — 保留简要申请/输注信息
if (isOutbound) {
if (d.bizType === 'pilot_service') {
const pilotDetail: OutboundQcDetail = {
// outbound:中试服务 / 临床应用 — 与出库质检一致,码值展示中文标签
if (isOutbound && (d.bizType === 'pilot_service' || d.bizType === 'clinical_application')) {
const outboundDetail: OutboundQcDetail = {
workOrder: {} as OutboundQcDetail['workOrder'],
application: d.application,
patient: d.patient,
@@ -1021,51 +1023,9 @@
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 || '—' }
]
})
}
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,87 +2,53 @@
<!-- 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>刷新
<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"
>
<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"
<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>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
show-overflow-tooltip
</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"
>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
{{ 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"
@click="handleStart(row)"
size="small"
@click.stop="handleStart(row)"
>
<el-icon><VideoPlay /></el-icon>开始
</el-button>
@@ -90,12 +56,19 @@
v-if="canFillResult(row)"
class="btn-op-item primary"
type="primary"
@click="openResult(row)"
size="small"
@click.stop="openResult(row)"
>
<el-icon><EditPen /></el-icon>填写结果
</el-button>
</template>
<el-button v-else class="btn-op-item primary" @click="handleView(row)">
<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
@@ -103,7 +76,7 @@
trigger="hover"
@command="(cmd) => handleDropdownCommand(cmd, row)"
>
<el-button class="btn-op-item">
<el-button class="btn-op-item" size="small" @click.stop>
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
@@ -118,15 +91,27 @@
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
</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 {