Files
zkxw/web-admin/src/views/pilot-service/pilot-application/ApplicationDetailDialog.vue
T
2026-06-16 13:01:17 +08:00

3818 lines
135 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 中试服务申请单详情弹窗 -->
<template>
<el-dialog
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
width="1600px"
class="dialog-form-layout application-detail-dialog"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
>
<template #header>
<div class="detail-dialog-header">
<div class="detail-dialog-header-left">
<h3>{{ isCreateMode ? '新增中试服务申请单' : '申请单详情' }}</h3>
<span v-if="!isCreateMode && applicationDetail.applicationNo" class="detail-code">
{{ applicationDetail.applicationNo }}
</span>
<el-tag
v-if="!isCreateMode && applicationDetail.applicationStatus"
:type="applicationStatusTagType(applicationDetail.applicationStatus)"
size="large"
>
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
</div>
</div>
</template>
<div class="application-detail-body" v-loading="detailLoading" element-loading-text="加载中…">
<!-- detail-tabs sticky 导航 -->
<div class="detail-tabs">
<div
v-for="tab in isCreateMode ? createDetailTabs : detailTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="scrollToTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
<!-- ═══ 创建模式表单 ═══ -->
<template v-if="isCreateMode">
<!-- Tab 1: 基本信息 -->
<div id="tab-basic" class="form-tab-pane">
<!-- 申请单信息 -->
<div class="section-card section-card--white">
<div class="section-title"><h3>申请单信息</h3></div>
<el-form
ref="createFormRef"
:model="createForm"
:rules="createFormRules"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="渠道" prop="channelId">
<el-select
v-model="createForm.channelId"
placeholder="请选择渠道"
filterable
clearable
:loading="createChannelLoading"
@change="handleCreateChannelChange"
>
<el-option
v-for="ch in createChannelOptions"
:key="ch.id"
:label="ch.channelName || ch.name"
:value="ch.id"
/>
</el-select>
</el-form-item>
<el-form-item label="服务订单" prop="channelServiceOrderId">
<el-select
v-model="createForm.channelServiceOrderId"
placeholder="请先选择渠道,再选择服务订单"
filterable
clearable
:disabled="!createForm.channelId"
:loading="createServiceOrderLoading"
>
<el-option
v-for="so in createServiceOrderOptions"
:key="so.id"
:label="formatServiceOrderLabel(so)"
:value="so.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="createForm.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
/>
</el-form-item>
</div>
</el-form>
</div>
<!-- 患者信息 -->
<div class="section-card section-card--white" style="margin-top: 16px">
<div class="section-title">
<h3>患者信息</h3>
<el-button type="primary" plain size="small" @click="openCustomerDialog">
<el-icon><User /></el-icon>
选择客户
</el-button>
</div>
<el-form
ref="patientFormRef"
:model="patientForm"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="姓名" prop="patientName">
<el-input v-model="patientForm.patientName" placeholder="请输入患者姓名" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="patientForm.sex" placeholder="请选择" clearable>
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="patientForm.age"
:min="0"
:max="200"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="身份证号" prop="idCardNo">
<el-input v-model="patientForm.idCardNo" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="联系电话" prop="phoneNo">
<el-input v-model="patientForm.phoneNo" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="紧急联系人" prop="emergencyContact">
<el-input v-model="patientForm.emergencyContact" placeholder="请输入紧急联系人" />
</el-form-item>
<el-form-item label="紧急联系人电话" prop="emergencyPhone">
<el-input
v-model="patientForm.emergencyPhone"
placeholder="请输入紧急联系人电话"
/>
</el-form-item>
<el-form-item label="是否需要体检" prop="needCheckup">
<el-switch
v-model="patientForm.needCheckup"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="临床诊断" prop="clinicalDiagnosis" class="form-col-span-2">
<el-input
v-model="patientForm.clinicalDiagnosis"
type="textarea"
placeholder="请输入临床诊断"
:rows="3"
/>
</el-form-item>
<el-form-item label="诊断附件" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
:show-file-list="false"
:auto-upload="false"
:on-change="handleDiagnosisFileChange"
class="upload-trigger"
>
<div class="upload-card">
<el-icon :size="28" color="#047857"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, idx) in diagnosisFileList"
:key="idx"
class="attachment-item"
>
<div class="attachment-preview">
<template v-if="isImageFile(file.url)">
<img :src="file.url" :alt="file.name" />
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click.stop="downloadFile(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click.stop="removeDiagnosisFile(idx)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel 等常见文件格式</span>
</div>
</el-form-item>
</div>
</el-form>
</div>
</div>
<!-- Tab 2: 输注方案(创建模式本地暂存) -->
<div id="tab-infusion-create" class="form-tab-pane">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="openLocalPlanDialog()">
<el-icon><Plus /></el-icon>
新增输注方案
</el-button>
</div>
</div>
<el-table :data="localPlanList" stripe row-key="_localId" class="list-table">
<el-table-column
prop="cellType"
label="细胞类型"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="specText" label="规格" min-width="120" show-overflow-tooltip />
<el-table-column
prop="totalCourseCount"
label="总疗程数"
min-width="80"
align="center"
/>
<el-table-column
prop="courseInterval"
label="疗程间隔"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="infusionRoute"
label="输注途径"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="infusionStartDate"
label="开始日期"
min-width="120"
align="center"
>
<template #default="{ row }">
{{ displayValue(row.infusionStartDate) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="160" fixed="right" align="center">
<template #default="{ row, $index }">
<el-button class="btn-op-item" @click="openLocalPlanDialog(row, $index)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item op-warn" @click="removeLocalPlan($index)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</template>
<template v-if="!isCreateMode && applicationDetail.applicationNo">
<!-- KPI 摘要条 -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-card__icon"
><el-icon><Document /></el-icon
></div>
<div class="kpi-card__main">
<div class="kpi-label">申请单号</div>
<div class="kpi-value kpi-value--emphasis">{{
displayValue(applicationDetail.applicationNo)
}}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"
><el-icon><CircleCheck /></el-icon
></div>
<div class="kpi-card__main">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<el-tag
:type="applicationStatusTagType(applicationDetail.applicationStatus)"
size="small"
>
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"
><el-icon><User /></el-icon
></div>
<div class="kpi-card__main">
<div class="kpi-label">渠道名称</div>
<div class="kpi-value">{{ displayValue(applicationDetail.channelName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon"
><el-icon><Calendar /></el-icon
></div>
<div class="kpi-card__main">
<div class="kpi-label">创建时间</div>
<div class="kpi-value">{{ displayValue(applicationDetail.createTime) }}</div>
</div>
</div>
</div>
<!-- ═══ Tab 1: 基本信息 ═══ -->
<div id="tab-basic" class="form-tab-pane">
<!-- 申请单信息(只读,data-grid) -->
<div class="section-card section-card--white">
<div class="section-title"><h3>申请单信息</h3></div>
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">申请单号</div>
<div class="data-value">{{ displayValue(applicationDetail.applicationNo) }}</div>
</div>
<div class="data-item">
<div class="data-label">渠道名称</div>
<div class="data-value">{{ displayValue(applicationDetail.channelName) }}</div>
</div>
<div class="data-item">
<div class="data-label">服务订单名称</div>
<div class="data-value">{{ displayValue(applicationDetail.serviceOrderNo) }}</div>
</div>
<div class="data-item">
<div class="data-label">状态</div>
<div class="data-value">
<el-tag
v-if="applicationDetail.applicationStatus"
:type="applicationStatusTagType(applicationDetail.applicationStatus)"
size="small"
>
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
<span v-else>-</span>
</div>
</div>
<div class="data-item">
<div class="data-label">创建时间</div>
<div class="data-value">{{ displayValue(applicationDetail.createTime) }}</div>
</div>
<div class="data-item">
<div class="data-label">备注</div>
<div class="data-value">{{ displayValue(applicationDetail.remark) }}</div>
</div>
</div>
</div>
<!-- 患者信息(可编辑) -->
<div class="detail-section">
<div class="detail-section-header">
<h3 class="detail-section-title">患者信息</h3>
<el-button type="primary" plain size="small" @click="openCustomerDialog">
<el-icon><User /></el-icon>
选择客户
</el-button>
</div>
<el-form
ref="patientFormRef"
:model="patientForm"
label-position="top"
class="dialog-form-layout"
>
<div class="form-grid-2">
<el-form-item label="姓名" prop="patientName">
<el-input v-model="patientForm.patientName" placeholder="请输入患者姓名" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="patientForm.sex" placeholder="请选择" clearable>
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number
v-model="patientForm.age"
:min="0"
:max="200"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="身份证号" prop="idCardNo">
<el-input v-model="patientForm.idCardNo" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="联系电话" prop="phoneNo">
<el-input v-model="patientForm.phoneNo" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="紧急联系人" prop="emergencyContact">
<el-input v-model="patientForm.emergencyContact" placeholder="请输入紧急联系人" />
</el-form-item>
<el-form-item label="紧急联系人电话" prop="emergencyPhone">
<el-input
v-model="patientForm.emergencyPhone"
placeholder="请输入紧急联系人电话"
/>
</el-form-item>
<el-form-item label="是否需要体检" prop="needCheckup">
<el-switch
v-model="patientForm.needCheckup"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="临床诊断" prop="clinicalDiagnosis" class="form-col-span-2">
<el-input
v-model="patientForm.clinicalDiagnosis"
type="textarea"
placeholder="请输入临床诊断"
:rows="3"
/>
</el-form-item>
<el-form-item label="诊断附件" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
:show-file-list="false"
:auto-upload="false"
:on-change="handleDiagnosisFileChange"
class="upload-trigger"
>
<div class="upload-card">
<el-icon :size="28" color="#047857"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, idx) in diagnosisFileList"
:key="idx"
class="attachment-item"
>
<div class="attachment-preview">
<template v-if="isImageFile(file.url)">
<img :src="file.url" :alt="file.name" />
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click.stop="downloadFile(file)"
>
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click.stop="removeDiagnosisFile(idx)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel 等常见文件格式</span>
</div>
</el-form-item>
</div>
<div class="detail-section-footer">
<el-button type="primary" :loading="patientSaving" @click="handleSavePatient">
<el-icon><Check /></el-icon>保存患者信息
</el-button>
</div>
</el-form>
</div>
</div>
<!-- ═══ Tab 2: 输注方案 ═══ -->
<div id="tab-infusion" class="form-tab-pane">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="openPlanDialog()">
<el-icon><Plus /></el-icon>
新增输注方案
</el-button>
</div>
</div>
<el-table :data="planList" v-loading="planLoading" stripe row-key="id" class="list-table">
<el-table-column type="expand">
<template #default="{ row: plan }">
<div class="expand-content">
<el-table
:data="scheduleMap[plan.id] || []"
v-loading="scheduleLoadingMap[plan.id]"
stripe
size="small"
class="list-table inner-table"
>
<el-table-column prop="orderNum" label="序号" min-width="60" align="center" />
<el-table-column
prop="cellType"
label="细胞类型"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="specText"
label="规格"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="infusionTime"
label="输注时间"
min-width="160"
align="center"
>
<template #default="{ row }">
{{ displayValue(row.infusionTime) }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="row.scheduleStatus && row.scheduleStatus !== 'dsc'"
:type="scheduleStatusTagType(row.scheduleStatus)"
size="small"
>
{{ scheduleStatusLabel(row.scheduleStatus) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="320" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="openScheduleDialog(row, plan)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-popconfirm
title="确定删除该计划吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteSchedule(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
<el-button
v-if="row.scheduleStatus === 'dsc'"
class="btn-op-item primary"
type="primary"
@click="handleMarkScheduleComplete(row, plan)"
>
<el-icon><CircleCheck /></el-icon>完成标记
</el-button>
<el-button
v-if="row.scheduleStatus === 'dsc'"
class="btn-op-item"
type="primary"
@click="handleCreateOutboundProduction(row, plan)"
>
<el-icon><Operation /></el-icon>创建生产工单
</el-button>
<el-button
v-if="row.scheduleStatus === 'ysc'"
class="btn-op-item"
@click="handleCreateOutboundQc(row)"
>
<el-icon><Tickets /></el-icon>创建质检工单
</el-button>
</template>
</el-table-column>
</el-table>
<div class="expand-footer">
<el-button
type="primary"
plain
size="small"
@click="openScheduleDialog(null, plan)"
>
<el-icon><Plus /></el-icon>
手动添加计划
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column
prop="cellType"
label="细胞类型"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="specText" label="规格" min-width="120" show-overflow-tooltip />
<el-table-column
prop="totalCourseCount"
label="总疗程数"
min-width="80"
align="center"
/>
<el-table-column
prop="courseInterval"
label="疗程间隔"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="infusionRoute"
label="输注途径"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="infusionStartDate"
label="开始日期"
min-width="120"
align="center"
>
<template #default="{ row }">
{{ displayValue(row.infusionStartDate) }}
</template>
</el-table-column>
<el-table-column label="进度" min-width="100" align="center">
<template #default="{ row }">
{{ getCompletedCount(row.id) }}/{{ getScheduleCount(row.id) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="160" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="openPlanDialog(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-popconfirm
title="确定删除该方案吗?将同时删除其下所有计划。"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeletePlan(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<!-- ═══ Tab 3: 体检信息 ═══ -->
<div id="tab-checkup" class="form-tab-pane">
<!-- 体检信息卡片 -->
<div class="detail-section">
<div class="detail-section-header">
<h3 class="detail-section-title">体检信息</h3>
<div v-if="checkupData" class="detail-section-actions">
<el-button type="primary" plain size="small" @click="openCheckupDialog">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
v-if="checkupData.checkupStatus === 'dtj'"
type="warning"
plain
size="small"
@click="handleMarkChecked"
>
<el-icon><CircleCheck /></el-icon>标记已体检
</el-button>
<el-button
v-if="checkupData.feeStatus === 'wjs'"
type="success"
plain
size="small"
@click="handleSettleFee"
>
<el-icon><Tickets /></el-icon>结算费用
</el-button>
</div>
<el-button v-else type="primary" size="small" @click="openCheckupDialog">
<el-icon><Plus /></el-icon>
新增体检信息
</el-button>
</div>
<template v-if="checkupData">
<el-descriptions :column="3" border>
<el-descriptions-item label="体检机构">
{{ displayValue(checkupData.institutionName) }}
</el-descriptions-item>
<el-descriptions-item label="预约时间">
{{ displayValue(checkupData.reserveTime) }}
</el-descriptions-item>
<el-descriptions-item label="预约费用">
{{ checkupData.reserveFee != null ? `${checkupData.reserveFee} 元` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="实际体检时间">
{{ displayValue(checkupData.actualCheckupTime) }}
</el-descriptions-item>
<el-descriptions-item label="实际体检费用">
{{
checkupData.actualCheckupFee != null
? `${checkupData.actualCheckupFee} 元`
: '-'
}}
</el-descriptions-item>
<el-descriptions-item label="体检状态">
<el-tag
v-if="checkupData.checkupStatus"
:type="checkupStatusTagType(checkupData.checkupStatus)"
size="small"
>
{{ checkupStatusLabel(checkupData.checkupStatus) }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="费用状态">
<el-tag
v-if="checkupData.feeStatus"
:type="feeStatusTagType(checkupData.feeStatus)"
size="small"
>
{{ feeStatusLabel(checkupData.feeStatus) }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="体检备注" :span="2">
{{ displayValue(checkupData.checkupRemark) }}
</el-descriptions-item>
<el-descriptions-item label="体检报告" :span="3">
<template v-if="checkupReportFiles.length">
<div class="attachment-list">
<a
v-for="(file, idx) in checkupReportFiles"
:key="idx"
:href="file.url"
target="_blank"
class="attachment-item attachment-link"
style="text-decoration: none"
>
<div class="attachment-preview">
<template v-if="isImageFile(file.url)">
<img :src="file.url" :alt="file.name" />
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click.stop="downloadFile(file)"
>
<Download />
</el-icon>
</div>
</div>
</a>
</div>
</template>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else description="暂无体检信息,请点击上方按钮新增" />
</div>
<!-- 预约项目明细 -->
<div v-if="checkupData" class="detail-section">
<div class="detail-section-header">
<h3 class="detail-section-title">预约项目明细</h3>
<el-button type="primary" plain size="small" @click="openReserveItemDialog">
<el-icon><Plus /></el-icon>
添加预约项目
</el-button>
</div>
<el-table
:data="reserveItems"
v-loading="reserveItemsLoading"
stripe
class="list-table"
>
<el-table-column
prop="itemName"
label="项目名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column label="项目费用(元)" min-width="140" align="center">
<template #default="{ row }">
{{ row.itemFee != null ? row.itemFee : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="100" align="center">
<template #default="{ row }">
<el-popconfirm
title="确定删除该项目吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteReserveItem(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn" size="small"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<!-- 实际项目明细 -->
<div v-if="checkupData && checkupData.checkupStatus === 'ytj'" class="detail-section">
<div class="detail-section-header">
<h3 class="detail-section-title">实际项目明细</h3>
<el-button type="primary" plain size="small" @click="openActualItemDialog">
<el-icon><Plus /></el-icon>
添加实际项目
</el-button>
</div>
<el-table :data="actualItems" v-loading="actualItemsLoading" stripe class="list-table">
<el-table-column
prop="itemName"
label="项目名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column label="项目费用(元)" min-width="140" align="center">
<template #default="{ row }">
{{ row.itemFee != null ? row.itemFee : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="100" align="center">
<template #default="{ row }">
<el-popconfirm
title="确定删除该项目吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteActualItem(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn" size="small"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- ═══ Tab 4: 交付信息 ═══ -->
<div id="tab-delivery" class="form-tab-pane">
<div class="list-toolbar">
<div class="list-toolbar-row" />
</div>
<el-table
:data="deliveryList"
v-loading="deliveryLoading"
stripe
row-key="scheduleId"
class="list-table"
>
<el-table-column prop="planOrderNum" label="方案序号" min-width="80" align="center" />
<el-table-column
prop="scheduleOrderNum"
label="计划序号"
min-width="80"
align="center"
/>
<el-table-column
prop="cellType"
label="细胞类型"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="specText" label="规格" min-width="120" show-overflow-tooltip />
<el-table-column label="输注时间" min-width="160" align="center">
<template #default="{ row }">
{{ displayValue(row.infusionTime) }}
</template>
</el-table-column>
<el-table-column label="计划状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="row.scheduleStatus && row.scheduleStatus !== 'dsc'"
:type="scheduleStatusTagType(row.scheduleStatus)"
size="small"
>
{{ scheduleStatusLabel(row.scheduleStatus) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="交付编号" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ displayValue(row.deliveryNo) }}
</template>
</el-table-column>
<el-table-column label="交付方式" min-width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.deliveryMode" type="primary" size="small">
{{ deliveryModeLabel(row.deliveryMode) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<template v-if="!row.deliveryNo">
<el-button
class="btn-main"
type="primary"
size="small"
@click="handleCreateDelivery(row)"
>
<el-icon><Plus /></el-icon>创建交付
</el-button>
</template>
<template v-else>
<el-button
class="btn-op-item"
size="small"
@click="openDeliveryDetailDialog(row)"
>
<el-icon><View /></el-icon>查看交付
</el-button>
<el-popconfirm
title="确定删除该交付吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteDelivery(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn" size="small"
><el-icon><Delete /></el-icon>删除交付</el-button
>
</template>
</el-popconfirm>
</template>
</template>
</el-table-column>
</el-table>
</div>
<!-- ═══ Tab: 放行审核 ═══ -->
<div id="tab-release" class="form-tab-pane">
<el-table
:data="releaseReviewList"
v-loading="releaseReviewLoading"
stripe
class="list-table"
>
<el-table-column
prop="infusionScheduleId"
label="输注计划ID"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="审核结论" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="row.reviewConclusion"
:type="releaseReviewConclusionTagType(row.reviewConclusion)"
size="small"
>
{{ releaseReviewConclusionLabel(row.reviewConclusion) }}
</el-tag>
<span v-else>待审核</span>
</template>
</el-table-column>
<el-table-column
prop="reviewRemark"
label="审核备注"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="审核时间" min-width="160" align="center">
<template #default="{ row }">
{{ displayValue(row.reviewTime) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<el-button
v-if="!row.reviewConclusion"
class="btn-op-item"
type="primary"
@click="handleReviewConclusion(row, 'tg')"
>
<el-icon><CircleCheck /></el-icon>通过
</el-button>
<el-button
v-if="!row.reviewConclusion"
class="btn-op-item op-warn"
@click="handleReviewConclusion(row, 'btg')"
>
<el-icon><CircleClose /></el-icon>不通过
</el-button>
<el-popconfirm
title="确定删除该审核记录吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteReleaseReview(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<!-- ═══ Tab: 应用确认 ═══ -->
<div id="tab-confirm" class="form-tab-pane">
<el-table
:data="infusionConfirmList"
v-loading="infusionConfirmLoading"
stripe
class="list-table"
>
<el-table-column
prop="infusionScheduleId"
label="输注计划ID"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="确认状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.confirmTime ? 'success' : 'info'" size="small">
{{ row.confirmTime ? '已确认' : '待确认' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="confirmDesc"
label="确认说明"
min-width="180"
show-overflow-tooltip
/>
<el-table-column label="确认时间" min-width="160" align="center">
<template #default="{ row }">
{{ displayValue(row.confirmTime) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<el-button
v-if="!row.confirmTime"
class="btn-op-item"
type="primary"
@click="handleConfirmInfusion(row)"
>
<el-icon><Check /></el-icon>确认
</el-button>
<el-button class="btn-op-item" @click="handleEditConfirmDesc(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-popconfirm
title="确定删除该确认记录吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteInfusionConfirm(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
<!-- ═══ 体检信息编辑弹窗 ═══ -->
<el-dialog
append-to-body
v-model="checkupDialogVisible"
:title="checkupData ? '编辑体检信息' : '新增体检信息'"
width="640px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form ref="checkupFormRef" :model="checkupForm" label-position="top" class="dialog-form">
<div class="form-grid-2">
<el-form-item label="预约时间">
<el-date-picker
v-model="checkupForm.reserveTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择预约时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="预约费用(元)">
<el-input-number
v-model="checkupForm.reserveFee"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="实际体检时间">
<el-date-picker
v-model="checkupForm.actualCheckupTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择实际体检时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="实际体检费用(元)">
<el-input-number
v-model="checkupForm.actualCheckupFee"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="体检备注" class="form-col-span-2">
<el-input
v-model="checkupForm.checkupRemark"
type="textarea"
placeholder="请输入体检备注"
:rows="3"
/>
</el-form-item>
<el-form-item label="体检报告" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
:show-file-list="false"
:auto-upload="false"
:on-change="handleCheckupDialogFileChange"
class="upload-trigger"
>
<div class="upload-card">
<el-icon :size="28" color="#047857"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, idx) in checkupDialogFileList"
:key="idx"
class="attachment-item"
>
<div class="attachment-preview">
<template v-if="isImageFile(file.url)">
<img :src="file.url" :alt="file.name" />
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<div class="attachment-actions">
<el-icon class="action-btn" title="下载" @click.stop="downloadFile(file)">
<Download />
</el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click.stop="removeCheckupDialogFile(idx)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel 等常见文件格式</span>
</div>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="checkupDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="checkupSaving" @click="handleSaveCheckup"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 预约项目添加弹窗 ═══ -->
<el-dialog
append-to-body
v-model="reserveItemDialogVisible"
title="添加预约项目"
width="420px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form :model="reserveItemForm" label-position="top" class="dialog-form">
<el-form-item label="项目名称">
<el-input v-model="reserveItemForm.itemName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="项目费用(元)">
<el-input-number
v-model="reserveItemForm.itemFee"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="reserveItemDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="reserveItemSaving" @click="handleSaveReserveItem"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 实际项目添加弹窗 ═══ -->
<el-dialog
append-to-body
v-model="actualItemDialogVisible"
title="添加实际项目"
width="420px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form :model="actualItemForm" label-position="top" class="dialog-form">
<el-form-item label="项目名称">
<el-input v-model="actualItemForm.itemName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="项目费用(元)">
<el-input-number
v-model="actualItemForm.itemFee"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="actualItemDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="actualItemSaving" @click="handleSaveActualItem"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 交付详情弹窗 ═══ -->
<el-dialog
append-to-body
v-model="deliveryDetailDialogVisible"
title="交付详情"
width="720px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-tabs v-model="deliveryDetailTab">
<el-tab-pane label="物料清单" name="materials">
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button type="primary" plain size="small" @click="openAddMaterialDialog">
<el-icon><Plus /></el-icon>
添加物料
</el-button>
</div>
</div>
<el-table
:data="deliveryMaterials"
v-loading="deliveryMaterialsLoading"
stripe
class="list-table"
>
<el-table-column
prop="materialName"
label="物料名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="数量" min-width="80" align="center">
<template #default="{ row }">
{{ row.quantity ?? '-' }}
</template>
</el-table-column>
<el-table-column label="单位" min-width="80" align="center">
<template #default="{ row }">
{{ displayValue(row.unitName) }}
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
{{ displayValue(row.remark) }}
</template>
</el-table-column>
<el-table-column label="操作" min-width="100" align="center">
<template #default="{ row }">
<el-popconfirm
title="确定删除该物料吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDeleteMaterial(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn" size="small"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="交付方式" name="method">
<el-form :model="deliveryMethodForm" label-position="top" class="dialog-form">
<el-form-item label="交付方式">
<el-radio-group v-model="deliveryMethodForm.deliveryMode">
<el-radio value="qdzt">渠道自提</el-radio>
<el-radio value="kdfh">快递发货</el-radio>
</el-radio-group>
</el-form-item>
<div class="form-grid-2">
<el-form-item label="交付附件" class="form-col-span-2">
<div class="attachment-upload-row">
<div
v-for="(file, idx) in deliveryMethodFileList"
:key="idx"
class="attachment-card"
>
<el-icon class="attachment-icon"><Document /></el-icon>
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<el-button
link
type="danger"
size="small"
@click="removeDeliveryMethodFile(idx)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-upload
:show-file-list="false"
:auto-upload="false"
:on-change="handleDeliveryMethodFileChange"
class="attachment-trigger"
>
<div class="attachment-trigger-card">
<el-icon size="20"><Plus /></el-icon>
<span>上传附件</span>
</div>
</el-upload>
</div>
</el-form-item>
<el-form-item label="发运时间">
<el-date-picker
v-model="deliveryMethodForm.shipTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择发运时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="交接温度">
<el-input
v-model="deliveryMethodForm.handoverTemperature"
placeholder="请输入交接温度"
/>
</el-form-item>
<template v-if="deliveryMethodForm.deliveryMode === 'qdzt'">
<el-form-item label="自提人员">
<el-input
v-model="deliveryMethodForm.pickupPerson"
placeholder="请输入自提人员"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="deliveryMethodForm.pickupPhone" placeholder="请输入联系电话" />
</el-form-item>
</template>
<template v-if="deliveryMethodForm.deliveryMode === 'kdfh'">
<el-form-item label="发货人">
<el-input v-model="deliveryMethodForm.senderName" placeholder="请输入发货人" />
</el-form-item>
<el-form-item label="快递公司">
<el-input
v-model="deliveryMethodForm.expressCompany"
placeholder="请输入快递公司"
/>
</el-form-item>
<el-form-item label="快递单号">
<el-input v-model="deliveryMethodForm.expressNo" placeholder="请输入快递单号" />
</el-form-item>
<el-form-item label="快递费用(元)">
<el-input-number
v-model="deliveryMethodForm.expressFee"
:min="0"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="预计到达时间">
<el-date-picker
v-model="deliveryMethodForm.estimatedArrivalTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择预计到达时间"
style="width: 100%"
/>
</el-form-item>
</template>
</div>
</el-form>
<div class="detail-section-footer">
<el-button
type="primary"
:loading="deliveryMethodSaving"
@click="handleSaveDeliveryMethod"
>
<el-icon><Check /></el-icon>保存交付方式
</el-button>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!-- ═══ 物料添加弹窗 ═══ -->
<el-dialog
append-to-body
v-model="addMaterialDialogVisible"
title="添加物料"
width="420px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form :model="addMaterialForm" label-position="top" class="dialog-form">
<el-form-item label="物料名称">
<el-input v-model="addMaterialForm.materialName" placeholder="请输入物料名称" />
</el-form-item>
<el-form-item label="数量">
<el-input-number
v-model="addMaterialForm.quantity"
:min="1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="单位">
<el-input v-model="addMaterialForm.unitName" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="addMaterialForm.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addMaterialDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="addMaterialSaving" @click="handleSaveMaterial"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 客户选择弹窗 ═══ -->
<el-dialog
append-to-body
v-model="customerDialogVisible"
title="选择客户"
width="800px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<div class="customer-dialog-content">
<el-form :inline="true" class="filter-form" @submit.prevent>
<el-form-item label="姓名">
<el-input
v-model="customerSearch.keyword"
placeholder="客户姓名"
clearable
@keyup.enter="loadCustomerList"
/>
</el-form-item>
<el-form-item label="手机号">
<el-input
v-model="customerSearch.phoneNo"
placeholder="手机号"
clearable
@keyup.enter="loadCustomerList"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadCustomerList">
<el-icon><Search /></el-icon>搜索
</el-button>
</el-form-item>
</el-form>
<el-table
:data="customerList"
v-loading="customerLoading"
stripe
highlight-current-row
@current-change="handleCustomerSelect"
class="list-table"
>
<el-table-column prop="customerName" label="姓名" min-width="120" />
<el-table-column prop="sex" label="性别" min-width="80" align="center" />
<el-table-column prop="phoneNo" label="手机号" min-width="130" />
<el-table-column prop="idCardNo" label="身份证号" min-width="180" show-overflow-tooltip />
</el-table>
<div v-if="customerTotal > customerPageSize" class="pagination list-pager-bar">
<el-pagination
:current-page="customerPage"
:page-size="customerPageSize"
:total="customerTotal"
layout="prev, pager, next"
@current-change="
(val: number) => {
customerPage = val
loadCustomerList()
}
"
/>
</div>
</div>
<template #footer>
<el-button @click="customerDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :disabled="!selectedCustomer" @click="applyCustomerToPatient">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
<!-- ═══ 输注方案弹窗 ═══ -->
<el-dialog
append-to-body
v-model="planDialogVisible"
:title="planDialogMode === 'add' ? '新增输注方案' : '编辑输注方案'"
width="640px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
@close="planFormRef?.resetFields()"
>
<el-form
ref="planFormRef"
:model="planForm"
:rules="planRules"
label-position="top"
class="dialog-form"
>
<div class="form-grid-2">
<el-form-item label="细胞类型" prop="cellType">
<el-input v-model="planForm.cellType" placeholder="请输入细胞类型" />
</el-form-item>
<el-form-item label="规格" prop="specText">
<el-input v-model="planForm.specText" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="总疗程数" prop="totalCourseCount">
<el-input-number
v-model="planForm.totalCourseCount"
:min="1"
:max="999"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="疗程间隔" prop="courseInterval">
<el-input v-model="planForm.courseInterval" placeholder="请输入疗程间隔" />
</el-form-item>
<el-form-item label="输注途径" prop="infusionRoute">
<el-input v-model="planForm.infusionRoute" placeholder="请输入输注途径" />
</el-form-item>
<el-form-item label="开始日期" prop="infusionStartDate">
<el-date-picker
v-model="planForm.infusionStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
style="width: 100%"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="planDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="planSaving" @click="handleSavePlan"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 输注计划编辑弹窗 ═══ -->
<el-dialog
append-to-body
v-model="scheduleDialogVisible"
:title="scheduleDialogMode === 'add' ? '添加输注计划' : '编辑输注计划'"
width="500px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form
ref="scheduleFormRef"
:model="scheduleForm"
label-position="top"
class="dialog-form"
>
<el-form-item label="输注时间" prop="infusionTime">
<el-date-picker
v-model="scheduleForm.infusionTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择输注时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="scheduleForm.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
/>
</el-form-item>
<el-form-item label="附件">
<div class="attachment-upload-row">
<div v-for="(file, idx) in scheduleFileList" :key="idx" class="attachment-card">
<el-icon class="attachment-icon"><Document /></el-icon>
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<el-button link type="danger" size="small" @click="removeScheduleFile(idx)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-upload
:show-file-list="false"
:auto-upload="false"
:on-change="handleScheduleFileChange"
class="attachment-trigger"
>
<div class="attachment-trigger-card">
<el-icon size="20"><Plus /></el-icon>
<span>上传附件</span>
</div>
</el-upload>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="scheduleDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="scheduleSaving" @click="handleSaveSchedule"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 创建模式-本地输注方案弹窗 ═══ -->
<el-dialog
append-to-body
v-model="localPlanDialogVisible"
:title="localPlanEditIndex >= 0 ? '编辑输注方案' : '新增输注方案'"
width="640px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form
ref="localPlanFormRef"
:model="localPlanForm"
:rules="localPlanRules"
label-position="top"
>
<div class="form-grid-2">
<el-form-item label="细胞类型" prop="cellType">
<el-input v-model="localPlanForm.cellType" placeholder="请输入细胞类型" />
</el-form-item>
<el-form-item label="规格" prop="specText">
<el-input v-model="localPlanForm.specText" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="总疗程数" prop="totalCourseCount">
<el-input-number
v-model="localPlanForm.totalCourseCount"
:min="1"
:max="999"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="疗程间隔" prop="courseInterval">
<el-input v-model="localPlanForm.courseInterval" placeholder="请输入疗程间隔" />
</el-form-item>
<el-form-item label="输注途径" prop="infusionRoute">
<el-input v-model="localPlanForm.infusionRoute" placeholder="请输入输注途径" />
</el-form-item>
<el-form-item label="开始日期" prop="infusionStartDate">
<el-date-picker
v-model="localPlanForm.infusionStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
style="width: 100%"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="localPlanDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" @click="handleSaveLocalPlan"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ 新增客户弹窗 ═══ -->
<el-dialog
append-to-body
v-model="newCustomerDialogVisible"
title="新增客户"
width="500px"
destroy-on-close
align-center
modal-class="protocol-dialog-mask"
class="dialog-form-layout"
style="backdrop-filter: none"
:close-on-click-modal="false"
>
<el-form
ref="newCustomerFormRef"
:model="newCustomerForm"
:rules="newCustomerRules"
label-position="top"
>
<div class="form-grid-2">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="newCustomerForm.customerName" placeholder="请输入" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="newCustomerForm.sex" clearable>
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="年龄">
<el-input-number
v-model="newCustomerForm.age"
:min="0"
:max="200"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="手机号" prop="phoneNo">
<el-input v-model="newCustomerForm.phoneNo" placeholder="请输入" />
</el-form-item>
<el-form-item label="身份证号">
<el-input v-model="newCustomerForm.idCardNo" placeholder="请输入" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="newCustomerDialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button type="primary" :loading="newCustomerSaving" @click="handleCreateCustomer"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
<!-- ═══ Footer Slot ═══ -->
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
<template v-if="isCreateMode">
<el-button @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button type="primary" :loading="createSubmitting" @click="handleCreateSubmit">
<el-icon><Check /></el-icon>保存
</el-button>
</template>
<template v-if="!isCreateMode">
<el-button
v-if="applicationDetail.applicationStatus === 'dks'"
type="primary"
@click="handleStart"
>
<el-icon><VideoPlay /></el-icon>开始
</el-button>
<el-button
v-if="applicationDetail.applicationStatus === 'jxz'"
type="primary"
@click="handleComplete"
>
<el-icon><CircleCheck /></el-icon>完成
</el-button>
<el-button
v-if="
applicationDetail.applicationStatus === 'dks' ||
applicationDetail.applicationStatus === 'jxz'
"
type="danger"
@click="handleVoid"
>
<el-icon><CircleClose /></el-icon>作废
</el-button>
</template>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import {
Plus,
Search,
User,
Document,
Delete,
VideoPlay,
CircleCheck,
CircleClose,
Calendar,
Edit,
Check,
Close,
View,
Operation,
Tickets
} from '@element-plus/icons-vue'
import {
fetchPilotApplicationDetail,
startPilotApplication,
completePilotApplication,
voidPilotApplication,
createPilotApplication,
fetchChannelList,
fetchChannelServiceOrderList,
fetchPilotPatient,
savePilotPatient,
fetchPilotInfusionPlanList,
createPilotInfusionPlan,
updatePilotInfusionPlan,
deletePilotInfusionPlan,
fetchPilotInfusionScheduleList,
autoGenerateSchedules,
updatePilotInfusionSchedule,
deletePilotInfusionSchedule,
markScheduleComplete,
fetchCustomerList,
fetchPilotCheckupDetail,
savePilotCheckup,
markPilotCheckupChecked,
settlePilotCheckupFee,
fetchPilotCheckupReserveItems,
savePilotCheckupReserveItems,
deletePilotCheckupReserveItem,
fetchPilotCheckupActualItems,
savePilotCheckupActualItems,
deletePilotCheckupActualItem,
fetchPilotDeliveryListByApplication,
createPilotDelivery,
deletePilotDelivery,
fetchPilotDeliveryMaterials,
savePilotDeliveryMaterials,
fetchPilotDeliveryMethod,
savePilotDeliveryMethod,
createOutboundProductionWorkOrder,
createOutboundQcWorkOrder,
fetchInfusionConfirmList,
confirmInfusion,
updateInfusionConfirm,
deleteInfusionConfirm
} from '@/api/admin'
import { fetchReleaseReviewList, updateReleaseReview, deleteReleaseReview } from '@/api/outbound-release-review'
import { fetchUploadAttachment } from '@/api/business'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import {
applicationStatusLabel,
applicationStatusTagType,
checkupStatusLabel,
checkupStatusTagType,
feeStatusLabel,
feeStatusTagType,
deliveryModeLabel,
scheduleStatusLabel,
scheduleStatusTagType,
releaseReviewConclusionLabel,
releaseReviewConclusionTagType,
channelServiceOrderTypeLabel
} from '@/utils/status-tag-registry'
import { getPilotBizConfig } from '@/config/pilot-biz-type'
defineOptions({ name: 'ApplicationDetailDialog' })
const props = defineProps<{
modelValue: boolean
applicationId: string
mode?: 'view' | 'create'
listRow?: any
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'status-changed'): void
(e: 'created', newId: string): void
}>()
const isCreateMode = computed(() => props.mode === 'create')
// ── 工具函数 ──────────────────────────────────────────────────────────
const displayValue = (v: any): string => {
if (v == null || v === '' || v === undefined) return '-'
return String(v)
}
const unwrapApiList = (res: any): any[] => {
const d = res?.data ?? res
if (Array.isArray(d)) return d
return d?.list ?? d?.records ?? []
}
// ── 数据 ──────────────────────────────────────────────────────────────
const detailBizConfig = computed(() =>
getPilotBizConfig(applicationDetail.value?.bizType)
)
const detailTabs = computed(() => {
const cfg = detailBizConfig.value
const tabs: Array<{ key: string; label: string }> = [
{ key: 'basic', label: '基本信息' },
{ key: 'infusion', label: cfg.planLabel }
]
if (cfg.showCheckup) {
tabs.push({ key: 'checkup', label: '体检信息' })
}
if (cfg.showDelivery) {
tabs.push({ key: 'delivery', label: '交付信息' })
}
tabs.push({ key: 'release', label: '放行审核' })
if (cfg.showInfusionConfirm) {
tabs.push({ key: 'confirm', label: '应用确认' })
}
return tabs
})
const createDetailTabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'infusion-create', label: '输注方案' }
]
const activeTab = ref('basic')
const scrollToTab = (key: string) => {
activeTab.value = key
const el = document.getElementById(`tab-${key}`)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
watch(activeTab, (name) => {
if (name === 'release') loadReleaseReviews()
if (name === 'confirm') loadInfusionConfirms()
})
const applicationDetail = ref<any>({})
const detailLoading = ref(false)
// ── 创建模式 ──────────────────────────────────────────────────────────
const createForm = reactive({
channelId: '',
channelServiceOrderId: '',
remark: ''
})
const createFormRef = ref<any>()
const createFormRules = {
channelId: [{ required: true, message: '请选择渠道', trigger: 'change' }],
channelServiceOrderId: [{ required: true, message: '请选择服务订单', trigger: 'change' }]
}
const createSubmitting = ref(false)
// ── 创建模式-本地输注方案 ─────────────────────────────────────────────
const localPlanList = ref<any[]>([])
const localPlanDialogVisible = ref(false)
const localPlanEditIndex = ref(-1)
const localPlanFormRef = ref<FormInstance>()
const localPlanForm = reactive({
cellType: '' as string,
specText: '' as string,
totalCourseCount: 1 as number,
courseInterval: '' as string,
infusionRoute: '' as string,
infusionStartDate: '' as string
})
const localPlanRules: FormRules = {
cellType: [{ required: true, message: '请输入细胞类型', trigger: 'blur' }],
totalCourseCount: [{ required: true, message: '请输入总疗程数', trigger: 'blur' }]
}
const openLocalPlanDialog = (row?: any, index?: number) => {
if (row && index !== undefined) {
localPlanEditIndex.value = index
localPlanForm.cellType = row.cellType || ''
localPlanForm.specText = row.specText || ''
localPlanForm.totalCourseCount = row.totalCourseCount ?? 1
localPlanForm.courseInterval = row.courseInterval || ''
localPlanForm.infusionRoute = row.infusionRoute || ''
localPlanForm.infusionStartDate = row.infusionStartDate || ''
} else {
localPlanEditIndex.value = -1
Object.assign(localPlanForm, {
cellType: '',
specText: '',
totalCourseCount: 1,
courseInterval: '',
infusionRoute: '',
infusionStartDate: ''
})
}
localPlanDialogVisible.value = true
}
const handleSaveLocalPlan = async () => {
try {
await localPlanFormRef.value?.validate()
} catch {
return
}
const item = { ...localPlanForm }
if (localPlanEditIndex.value >= 0) {
localPlanList.value[localPlanEditIndex.value] = {
...localPlanList.value[localPlanEditIndex.value],
...item
}
} else {
localPlanList.value.push({ _localId: Date.now().toString(), ...item })
}
localPlanDialogVisible.value = false
}
const removeLocalPlan = (index: number) => {
localPlanList.value.splice(index, 1)
}
const createChannelOptions = ref<any[]>([])
const createChannelLoading = ref(false)
const createServiceOrderOptions = ref<any[]>([])
const createServiceOrderLoading = ref(false)
const formatServiceOrderLabel = (so: any) => {
const name = (so.orderName || '').trim()
const typeLabel = channelServiceOrderTypeLabel(so.orderType)
if (name) return `${name}(${typeLabel})`
const no = so.id ? String(so.id).slice(-8) : ''
return `${no}(${typeLabel})`
}
const loadCreateChannels = async () => {
createChannelLoading.value = true
try {
createChannelOptions.value = await fetchAllPagedRecords((page, pageSize) =>
fetchChannelList({ page, pageSize })
)
} catch {
createChannelOptions.value = []
} finally {
createChannelLoading.value = false
}
}
const handleCreateChannelChange = async (channelId: string) => {
createForm.channelServiceOrderId = ''
createServiceOrderOptions.value = []
if (!channelId) return
createServiceOrderLoading.value = true
try {
createServiceOrderOptions.value = await fetchAllPagedRecords((page, pageSize) =>
fetchChannelServiceOrderList({ channelId, page, pageSize })
)
} catch {
createServiceOrderOptions.value = []
} finally {
createServiceOrderLoading.value = false
}
}
const handleCreateSubmit = async () => {
try {
await createFormRef.value?.validate()
} catch {
return
}
createSubmitting.value = true
try {
// 1. 创建申请单
const res: any = await createPilotApplication({
channelServiceOrderId: createForm.channelServiceOrderId,
remark: createForm.remark
})
const data = res?.data ?? res
const newId = data?.id || data?.pilotApplicationId || data
if (!newId) {
ElMessage.error('创建申请单成功,但未获取到申请单 ID')
return
}
// 2. 保存患者信息(若有填写)
const hasPatientData =
patientForm.patientName ||
patientForm.sex ||
patientForm.age !== null ||
patientForm.idCardNo ||
patientForm.phoneNo ||
patientForm.emergencyContact ||
patientForm.emergencyPhone ||
patientForm.clinicalDiagnosis ||
diagnosisFileList.value.length > 0
if (hasPatientData) {
try {
const attachments = diagnosisFileList.value.map((f) => f.url).join(',')
await savePilotPatient({
pilotApplicationId: String(newId),
customerId: patientForm.customerId || undefined,
patientName: patientForm.patientName || undefined,
sex: patientForm.sex || undefined,
age: patientForm.age ?? undefined,
idCardNo: patientForm.idCardNo || undefined,
phoneNo: patientForm.phoneNo || undefined,
emergencyContact: patientForm.emergencyContact || undefined,
emergencyPhone: patientForm.emergencyPhone || undefined,
clinicalDiagnosis: patientForm.clinicalDiagnosis || undefined,
diagnosisAttachments: attachments || undefined,
needCheckup: patientForm.needCheckup
})
} catch (e: any) {
console.error('保存患者信息失败:', e)
ElMessage.warning('申请单已创建,但患者信息保存失败,请在详情中补充')
}
}
// 3. 保存本地输注方案
if (localPlanList.value.length > 0) {
for (const plan of localPlanList.value) {
try {
await createPilotInfusionPlan({
pilotApplicationId: String(newId),
cellType: plan.cellType,
specText: plan.specText || undefined,
totalCourseCount: plan.totalCourseCount,
courseInterval: plan.courseInterval || undefined,
infusionRoute: plan.infusionRoute || undefined,
infusionStartDate: plan.infusionStartDate || undefined
})
} catch (e: any) {
console.error('保存输注方案失败:', e)
}
}
}
ElMessage.success('创建成功')
emit('created', String(newId))
} catch (e: any) {
ElMessage.error(e?.message || '创建失败')
} finally {
createSubmitting.value = false
}
}
// ── 加载申请单详情 ────────────────────────────────────────────────────
const loadApplicationDetail = async () => {
if (!props.applicationId) return
detailLoading.value = true
try {
const res: any = await fetchPilotApplicationDetail({ id: props.applicationId })
const data = res?.data ?? res
applicationDetail.value = data || {}
} catch (e) {
console.error('加载申请单详情失败:', e)
} finally {
detailLoading.value = false
}
}
// ── 状态操作 ──────────────────────────────────────────────────────────
const handleStart = async () => {
try {
await ElMessageBox.confirm('确定开始该申请单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await startPilotApplication({ id: props.applicationId })
ElMessage.success('操作成功')
loadApplicationDetail()
emit('status-changed')
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleComplete = async () => {
try {
await ElMessageBox.confirm('确定完成该申请单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await completePilotApplication({ id: props.applicationId })
ElMessage.success('操作成功')
loadApplicationDetail()
emit('status-changed')
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleVoid = async () => {
try {
await ElMessageBox.confirm('确定作废该申请单吗?作废后不可恢复。', '提示', {
confirmButtonText: '确定作废',
cancelButtonText: '取消',
type: 'warning'
})
await voidPilotApplication({ id: props.applicationId })
ElMessage.success('操作成功')
loadApplicationDetail()
emit('status-changed')
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
// ═══════════════════════════════════════════════════════════════════════
// 患者信息
// ═══════════════════════════════════════════════════════════════════════
const patientFormRef = ref<FormInstance>()
const patientSaving = ref(false)
interface DiagnosisFile {
name: string
url: string
}
const diagnosisFileList = ref<DiagnosisFile[]>([])
const initPatientForm = () => ({
customerId: '' as string,
patientName: '' as string,
sex: '' as string,
age: null as number | null,
idCardNo: '' as string,
phoneNo: '' as string,
emergencyContact: '' as string,
emergencyPhone: '' as string,
clinicalDiagnosis: '' as string,
diagnosisAttachments: '' as string,
needCheckup: 1 as number
})
const patientForm = reactive(initPatientForm())
const loadPatient = async () => {
if (!props.applicationId) return
try {
const res: any = await fetchPilotPatient({ pilotApplicationId: props.applicationId })
const data = res?.data ?? res
if (data && Object.keys(data).length > 0) {
patientForm.customerId = data.customerId || ''
patientForm.patientName = data.patientName || ''
patientForm.sex = data.sex || ''
patientForm.age = data.age ?? null
patientForm.idCardNo = data.idCardNo || ''
patientForm.phoneNo = data.phoneNo || ''
patientForm.emergencyContact = data.emergencyContact || ''
patientForm.emergencyPhone = data.emergencyPhone || ''
patientForm.clinicalDiagnosis = data.clinicalDiagnosis || ''
patientForm.diagnosisAttachments = data.diagnosisAttachments || ''
patientForm.needCheckup = data.needCheckup ?? 1
// 解析附件
diagnosisFileList.value = parseAttachmentUrls(patientForm.diagnosisAttachments)
}
} catch (e) {
console.error('加载患者信息失败:', e)
}
}
const parseAttachmentUrls = (urls: string): DiagnosisFile[] => {
if (!urls) return []
return urls
.split(',')
.filter(Boolean)
.map((url) => {
const parts = url.split('/')
const name = decodeURIComponent(parts[parts.length - 1] || '附件')
return { name, url }
})
}
const handleDiagnosisFileChange = async (uploadFile: any) => {
if (!uploadFile?.raw) return
try {
const formData = new FormData()
formData.append('file', uploadFile.raw)
const res: any = await fetchUploadAttachment(formData)
const fileUrl = res?.data?.fileUrl || res?.fileUrl || res?.data?.url || ''
if (fileUrl) {
diagnosisFileList.value.push({ name: uploadFile.name, url: fileUrl })
}
} catch (e: any) {
ElMessage.error(e?.message || '上传失败')
}
}
const removeDiagnosisFile = (idx: number) => {
diagnosisFileList.value.splice(idx, 1)
}
const handleSavePatient = async () => {
patientSaving.value = true
try {
const attachments = diagnosisFileList.value.map((f) => f.url).join(',')
await savePilotPatient({
pilotApplicationId: props.applicationId,
customerId: patientForm.customerId || undefined,
patientName: patientForm.patientName,
sex: patientForm.sex || undefined,
age: patientForm.age ?? undefined,
idCardNo: patientForm.idCardNo || undefined,
phoneNo: patientForm.phoneNo || undefined,
emergencyContact: patientForm.emergencyContact || undefined,
emergencyPhone: patientForm.emergencyPhone || undefined,
clinicalDiagnosis: patientForm.clinicalDiagnosis || undefined,
diagnosisAttachments: attachments || undefined,
needCheckup: patientForm.needCheckup
})
ElMessage.success('保存成功')
loadApplicationDetail()
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
patientSaving.value = false
}
}
// ── 客户选择弹窗 ──────────────────────────────────────────────────────
const customerDialogVisible = ref(false)
const customerLoading = ref(false)
const customerList = ref<any[]>([])
const customerPage = ref(1)
const customerPageSize = ref(10)
const customerTotal = ref(0)
const selectedCustomer = ref<any>(null)
const customerSearch = reactive({ keyword: '', phoneNo: '' })
const openCustomerDialog = () => {
customerSearch.keyword = ''
customerSearch.phoneNo = ''
customerPage.value = 1
selectedCustomer.value = null
customerDialogVisible.value = true
loadCustomerList()
}
const loadCustomerList = async () => {
customerLoading.value = true
try {
const keyword = customerSearch.keyword || customerSearch.phoneNo || undefined
const res: any = await fetchCustomerList({
page: customerPage.value,
pageSize: customerPageSize.value,
keyword
})
const data = res?.data ?? res
customerList.value = data?.list ?? data?.records ?? []
customerTotal.value = data?.total ?? 0
} catch {
customerList.value = []
} finally {
customerLoading.value = false
}
}
const handleCustomerSelect = (row: any) => {
selectedCustomer.value = row
}
const applyCustomerToPatient = () => {
const c = selectedCustomer.value
if (!c) return
patientForm.customerId = c.id || ''
patientForm.patientName = c.customerName || c.name || ''
patientForm.sex = c.sex || ''
patientForm.age = c.age ?? null
patientForm.idCardNo = c.idCardNo || ''
patientForm.phoneNo = c.phoneNo || ''
customerDialogVisible.value = false
ElMessage.success('已填充客户信息')
}
// ═══════════════════════════════════════════════════════════════════════
// 输注方案
// ═══════════════════════════════════════════════════════════════════════
const planList = ref<any[]>([])
const planLoading = ref(false)
const scheduleMap = ref<Record<string, any[]>>({})
const scheduleLoadingMap = ref<Record<string, boolean>>({})
const loadPlans = async () => {
if (!props.applicationId) return
planLoading.value = true
try {
const res: any = await fetchPilotInfusionPlanList({ pilotApplicationId: props.applicationId })
planList.value = unwrapApiList(res)
// 为每个方案加载计划
for (const plan of planList.value) {
await loadSchedulesForPlan(plan.id)
}
} catch (e) {
console.error('加载输注方案失败:', e)
} finally {
planLoading.value = false
}
}
const loadSchedulesForPlan = async (planId: string) => {
scheduleLoadingMap.value[planId] = true
try {
const res: any = await fetchPilotInfusionScheduleList({ infusionPlanId: planId })
scheduleMap.value[planId] = unwrapApiList(res)
} catch {
scheduleMap.value[planId] = []
} finally {
scheduleLoadingMap.value[planId] = false
}
}
const getScheduleCount = (planId: string): number => {
return (scheduleMap.value[planId] || []).length
}
const getCompletedCount = (planId: string): number => {
return (scheduleMap.value[planId] || []).filter((s: any) => s.scheduleStatus === 'ywc').length
}
// ── 方案弹窗 ──────────────────────────────────────────────────────────
const planDialogVisible = ref(false)
const planDialogMode = ref<'add' | 'edit'>('add')
const planSaving = ref(false)
const planFormRef = ref<FormInstance>()
const currentEditPlanId = ref('')
const initPlanForm = () => ({
cellType: '' as string,
specText: '' as string,
totalCourseCount: 1 as number,
courseInterval: '' as string,
infusionRoute: '' as string,
infusionStartDate: '' as string
})
const planForm = reactive(initPlanForm())
const planRules: FormRules = {
cellType: [{ required: true, message: '请输入细胞类型', trigger: 'blur' }],
totalCourseCount: [{ required: true, message: '请输入总疗程数', trigger: 'blur' }]
}
const openPlanDialog = (row?: any) => {
if (row) {
planDialogMode.value = 'edit'
currentEditPlanId.value = row.id
planForm.cellType = row.cellType || ''
planForm.specText = row.specText || ''
planForm.totalCourseCount = row.totalCourseCount ?? 1
planForm.courseInterval = row.courseInterval || ''
planForm.infusionRoute = row.infusionRoute || ''
planForm.infusionStartDate = row.infusionStartDate || ''
} else {
planDialogMode.value = 'add'
currentEditPlanId.value = ''
Object.assign(planForm, initPlanForm())
}
planDialogVisible.value = true
}
const handleSavePlan = async () => {
try {
await planFormRef.value?.validate()
} catch {
return
}
planSaving.value = true
try {
const payload = {
pilotApplicationId: props.applicationId,
cellType: planForm.cellType,
specText: planForm.specText || undefined,
totalCourseCount: planForm.totalCourseCount,
courseInterval: planForm.courseInterval || undefined,
infusionRoute: planForm.infusionRoute || undefined,
infusionStartDate: planForm.infusionStartDate || undefined
}
let planId = ''
if (planDialogMode.value === 'add') {
const res: any = await createPilotInfusionPlan(payload)
const data = res?.data ?? res
planId = data?.id || data
ElMessage.success('方案创建成功')
} else {
await updatePilotInfusionPlan({
id: currentEditPlanId.value,
...payload
})
planId = currentEditPlanId.value
ElMessage.success('方案更新成功')
}
planDialogVisible.value = false
await loadPlans()
// 新增方案后提示是否自动生成计划
if (planDialogMode.value === 'add' && planId && planForm.totalCourseCount > 0) {
try {
await ElMessageBox.confirm(
`是否立即生成 ${planForm.totalCourseCount} 条输注计划?`,
'提示',
{
confirmButtonText: '立即生成',
cancelButtonText: '稍后再说',
type: 'info'
}
)
await autoGenerateSchedules({ infusionPlanId: planId })
ElMessage.success('计划已生成')
await loadPlans()
} catch {
// 用户取消
}
}
} catch (e: any) {
ElMessage.error(e?.message || '操作失败')
} finally {
planSaving.value = false
}
}
const handleDeletePlan = async (row: any) => {
try {
await deletePilotInfusionPlan({ id: row.id })
ElMessage.success('删除成功')
loadPlans()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ── 计划弹窗 ──────────────────────────────────────────────────────────
const scheduleDialogVisible = ref(false)
const scheduleDialogMode = ref<'add' | 'edit'>('add')
const scheduleSaving = ref(false)
const scheduleFormRef = ref<FormInstance>()
const currentEditScheduleId = ref('')
const currentSchedulePlanId = ref('')
interface ScheduleFile {
name: string
url: string
}
const scheduleFileList = ref<ScheduleFile[]>([])
const scheduleForm = reactive({
infusionTime: '' as string,
remark: '' as string,
attachments: '' as string
})
const openScheduleDialog = (row: any | null, plan: any) => {
currentSchedulePlanId.value = plan.id
if (row) {
scheduleDialogMode.value = 'edit'
currentEditScheduleId.value = row.id
scheduleForm.infusionTime = row.infusionTime || ''
scheduleForm.remark = row.remark || ''
scheduleForm.attachments = row.attachments || ''
scheduleFileList.value = parseAttachmentUrls(scheduleForm.attachments)
} else {
scheduleDialogMode.value = 'add'
currentEditScheduleId.value = ''
scheduleForm.infusionTime = ''
scheduleForm.remark = ''
scheduleForm.attachments = ''
scheduleFileList.value = []
}
scheduleDialogVisible.value = true
}
const handleScheduleFileChange = async (uploadFile: any) => {
if (!uploadFile?.raw) return
try {
const formData = new FormData()
formData.append('file', uploadFile.raw)
const res: any = await fetchUploadAttachment(formData)
const fileUrl = res?.data?.fileUrl || res?.fileUrl || res?.data?.url || ''
if (fileUrl) {
scheduleFileList.value.push({ name: uploadFile.name, url: fileUrl })
}
} catch (e: any) {
ElMessage.error(e?.message || '上传失败')
}
}
const removeScheduleFile = (idx: number) => {
scheduleFileList.value.splice(idx, 1)
}
const handleSaveSchedule = async () => {
scheduleSaving.value = true
try {
const attachments = scheduleFileList.value.map((f) => f.url).join(',')
if (scheduleDialogMode.value === 'add') {
// 手动添加:生成下一条 orderNum
const existing = scheduleMap.value[currentSchedulePlanId.value] || []
const nextOrderNum = existing.length + 1
await autoGenerateSchedules({
infusionPlanId: currentSchedulePlanId.value,
manualSchedules: [
{
orderNum: nextOrderNum,
infusionTime: scheduleForm.infusionTime || undefined,
remark: scheduleForm.remark || undefined,
attachments: attachments || undefined
}
]
})
ElMessage.success('添加成功')
} else {
await updatePilotInfusionSchedule({
id: currentEditScheduleId.value,
infusionTime: scheduleForm.infusionTime || undefined,
remark: scheduleForm.remark || undefined,
attachments: attachments || undefined
})
ElMessage.success('更新成功')
}
scheduleDialogVisible.value = false
await loadSchedulesForPlan(currentSchedulePlanId.value)
} catch (e: any) {
ElMessage.error(e?.message || '操作失败')
} finally {
scheduleSaving.value = false
}
}
const handleDeleteSchedule = async (row: any) => {
try {
await deletePilotInfusionSchedule({ id: row.id })
ElMessage.success('删除成功')
// 刷新对应方案的计划
const planId = Object.entries(scheduleMap.value).find(([, schedules]) =>
schedules.some((s: any) => s.id === row.id)
)?.[0]
if (planId) await loadSchedulesForPlan(planId)
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
const handleMarkScheduleComplete = async (row: any, plan: any) => {
try {
await ElMessageBox.confirm('确定将该计划标记为已完成吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await markScheduleComplete({ id: row.id })
ElMessage.success('已标记完成')
await loadSchedulesForPlan(plan.id)
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleCreateOutboundProduction = async (row: any, plan: any) => {
try {
await ElMessageBox.confirm('确定为该计划创建出库生产工单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await createOutboundProductionWorkOrder({ scheduleId: row.id })
ElMessage.success('创建出库生产工单成功')
await loadSchedulesForPlan(plan.id)
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleCreateOutboundQc = async (row: any) => {
try {
await ElMessageBox.confirm('确定为该计划创建出库质检工单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await createOutboundQcWorkOrder({ scheduleId: row.id })
ElMessage.success('创建出库质检工单成功')
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
// ═══════════════════════════════════════════════════════════════════════
// 放行审核
// ═══════════════════════════════════════════════════════════════════════
const releaseReviewList = ref<any[]>([])
const releaseReviewLoading = ref(false)
const loadReleaseReviews = async () => {
if (!props.applicationId) return
releaseReviewLoading.value = true
try {
const res: any = await fetchReleaseReviewList({ scheduleId: props.applicationId })
releaseReviewList.value = unwrapApiList(res)
} catch {
releaseReviewList.value = []
} finally {
releaseReviewLoading.value = false
}
}
const handleReviewConclusion = async (row: any, conclusion: string) => {
try {
const msg = conclusion === 'tg' ? '通过' : '不通过'
await ElMessageBox.confirm(`确定将该审核标记为「${msg}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await updateReleaseReview({ id: row.id, reviewConclusion: conclusion })
ElMessage.success('审核完成')
await loadReleaseReviews()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleDeleteReleaseReview = async (row: any) => {
try {
await deleteReleaseReview({ id: row.id })
ElMessage.success('删除成功')
await loadReleaseReviews()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ═══════════════════════════════════════════════════════════════════════
// 应用确认
// ═══════════════════════════════════════════════════════════════════════
const infusionConfirmList = ref<any[]>([])
const infusionConfirmLoading = ref(false)
const loadInfusionConfirms = async () => {
if (!props.applicationId) return
infusionConfirmLoading.value = true
try {
const res: any = await fetchInfusionConfirmList({ applicationId: props.applicationId })
infusionConfirmList.value = unwrapApiList(res)
} catch {
infusionConfirmList.value = []
} finally {
infusionConfirmLoading.value = false
}
}
const handleConfirmInfusion = async (row: any) => {
try {
await ElMessageBox.confirm('确定标记为已确认吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await confirmInfusion({ id: row.id })
ElMessage.success('确认成功')
await loadInfusionConfirms()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleEditConfirmDesc = async (row: any) => {
try {
const { value } = await ElMessageBox.prompt('请输入确认说明', '编辑确认说明', {
inputValue: row.confirmDesc || '',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
await updateInfusionConfirm({ id: row.id, confirmDesc: value })
ElMessage.success('更新成功')
await loadInfusionConfirms()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleDeleteInfusionConfirm = async (row: any) => {
try {
await deleteInfusionConfirm({ id: row.id })
ElMessage.success('删除成功')
await loadInfusionConfirms()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ═══════════════════════════════════════════════════════════════════════
// 体检信息
// ═══════════════════════════════════════════════════════════════════════
const checkupData = ref<any>(null)
const checkupSaving = ref(false)
const checkupDialogVisible = ref(false)
const checkupFormRef = ref<FormInstance>()
interface CheckupFile {
name: string
url: string
}
const checkupReportFiles = ref<CheckupFile[]>([])
const checkupDialogFileList = ref<CheckupFile[]>([])
const initCheckupForm = () => ({
reserveTime: '' as string,
reserveFee: null as number | null,
actualCheckupTime: '' as string,
actualCheckupFee: null as number | null,
checkupRemark: '' as string,
reportAttachments: '' as string
})
const checkupForm = reactive(initCheckupForm())
const loadCheckup = async () => {
if (!props.applicationId) return
try {
const res: any = await fetchPilotCheckupDetail({ pilotApplicationId: props.applicationId })
const data = res?.data ?? res
if (data && Object.keys(data).length > 0 && data.id) {
checkupData.value = data
checkupReportFiles.value = parseAttachmentUrls(data.reportAttachments || '')
} else {
checkupData.value = null
checkupReportFiles.value = []
}
} catch {
checkupData.value = null
checkupReportFiles.value = []
}
}
const openCheckupDialog = () => {
if (checkupData.value) {
checkupForm.reserveTime = checkupData.value.reserveTime || ''
checkupForm.reserveFee = checkupData.value.reserveFee ?? null
checkupForm.actualCheckupTime = checkupData.value.actualCheckupTime || ''
checkupForm.actualCheckupFee = checkupData.value.actualCheckupFee ?? null
checkupForm.checkupRemark = checkupData.value.checkupRemark || ''
checkupForm.reportAttachments = checkupData.value.reportAttachments || ''
checkupDialogFileList.value = parseAttachmentUrls(checkupForm.reportAttachments)
} else {
Object.assign(checkupForm, initCheckupForm())
checkupDialogFileList.value = []
}
checkupDialogVisible.value = true
}
const handleCheckupDialogFileChange = async (uploadFile: any) => {
if (!uploadFile?.raw) return
try {
const formData = new FormData()
formData.append('file', uploadFile.raw)
const res: any = await fetchUploadAttachment(formData)
const fileUrl = res?.data?.fileUrl || res?.fileUrl || res?.data?.url || ''
if (fileUrl) {
checkupDialogFileList.value.push({ name: uploadFile.name, url: fileUrl })
}
} catch (e: any) {
ElMessage.error(e?.message || '上传失败')
}
}
const removeCheckupDialogFile = (idx: number) => {
checkupDialogFileList.value.splice(idx, 1)
}
const handleSaveCheckup = async () => {
checkupSaving.value = true
try {
const attachments = checkupDialogFileList.value.map((f) => f.url).join(',')
await savePilotCheckup({
applicationId: props.applicationId,
reserveTime: checkupForm.reserveTime || undefined,
reserveFee: checkupForm.reserveFee ?? undefined,
actualCheckupTime: checkupForm.actualCheckupTime || undefined,
actualCheckupFee: checkupForm.actualCheckupFee ?? undefined,
checkupRemark: checkupForm.checkupRemark || undefined,
reportAttachments: attachments || undefined
})
ElMessage.success('保存成功')
checkupDialogVisible.value = false
await loadCheckup()
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
checkupSaving.value = false
}
}
const handleMarkChecked = async () => {
try {
await ElMessageBox.confirm('确定标记为已体检吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await markPilotCheckupChecked({ id: checkupData.value.id })
ElMessage.success('已标记为已体检')
await loadCheckup()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
const handleSettleFee = async () => {
try {
await ElMessageBox.confirm('确定结算费用吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await settlePilotCheckupFee({ id: checkupData.value.id })
ElMessage.success('费用已结算')
await loadCheckup()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '操作失败')
}
}
// ── 预约项目 ──────────────────────────────────────────────────────────
const reserveItems = ref<any[]>([])
const reserveItemsLoading = ref(false)
const reserveItemDialogVisible = ref(false)
const reserveItemSaving = ref(false)
const reserveItemForm = reactive({ itemName: '', itemFee: null as number | null })
const loadReserveItems = async () => {
if (!checkupData.value?.id) return
reserveItemsLoading.value = true
try {
const res: any = await fetchPilotCheckupReserveItems({ pilotCheckupId: checkupData.value.id })
reserveItems.value = unwrapApiList(res)
} catch {
reserveItems.value = []
} finally {
reserveItemsLoading.value = false
}
}
const openReserveItemDialog = () => {
reserveItemForm.itemName = ''
reserveItemForm.itemFee = null
reserveItemDialogVisible.value = true
}
const handleSaveReserveItem = async () => {
if (!reserveItemForm.itemName) {
ElMessage.warning('请输入项目名称')
return
}
reserveItemSaving.value = true
try {
await savePilotCheckupReserveItems({
pilotCheckupId: checkupData.value.id,
items: [
{ itemName: reserveItemForm.itemName, itemFee: reserveItemForm.itemFee ?? undefined }
]
})
ElMessage.success('添加成功')
reserveItemDialogVisible.value = false
await loadReserveItems()
} catch (e: any) {
ElMessage.error(e?.message || '添加失败')
} finally {
reserveItemSaving.value = false
}
}
const handleDeleteReserveItem = async (row: any) => {
try {
await deletePilotCheckupReserveItem({ id: row.id })
ElMessage.success('删除成功')
await loadReserveItems()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ── 实际项目 ──────────────────────────────────────────────────────────
const actualItems = ref<any[]>([])
const actualItemsLoading = ref(false)
const actualItemDialogVisible = ref(false)
const actualItemSaving = ref(false)
const actualItemForm = reactive({ itemName: '', itemFee: null as number | null })
const loadActualItems = async () => {
if (!checkupData.value?.id) return
actualItemsLoading.value = true
try {
const res: any = await fetchPilotCheckupActualItems({ pilotCheckupId: checkupData.value.id })
actualItems.value = unwrapApiList(res)
} catch {
actualItems.value = []
} finally {
actualItemsLoading.value = false
}
}
const openActualItemDialog = () => {
actualItemForm.itemName = ''
actualItemForm.itemFee = null
actualItemDialogVisible.value = true
}
const handleSaveActualItem = async () => {
if (!actualItemForm.itemName) {
ElMessage.warning('请输入项目名称')
return
}
actualItemSaving.value = true
try {
await savePilotCheckupActualItems({
pilotCheckupId: checkupData.value.id,
items: [{ itemName: actualItemForm.itemName, itemFee: actualItemForm.itemFee ?? undefined }]
})
ElMessage.success('添加成功')
actualItemDialogVisible.value = false
await loadActualItems()
} catch (e: any) {
ElMessage.error(e?.message || '添加失败')
} finally {
actualItemSaving.value = false
}
}
const handleDeleteActualItem = async (row: any) => {
try {
await deletePilotCheckupActualItem({ id: row.id })
ElMessage.success('删除成功')
await loadActualItems()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ═══════════════════════════════════════════════════════════════════════
// 交付信息
// ═══════════════════════════════════════════════════════════════════════
const deliveryList = ref<any[]>([])
const deliveryLoading = ref(false)
const deliveryDetailDialogVisible = ref(false)
const deliveryDetailTab = ref('materials')
const currentDeliveryRow = ref<any>(null)
// 物料
const deliveryMaterials = ref<any[]>([])
const deliveryMaterialsLoading = ref(false)
const addMaterialDialogVisible = ref(false)
const addMaterialSaving = ref(false)
const addMaterialForm = reactive({
materialName: '',
quantity: 1 as number,
unitName: '',
remark: ''
})
// 交付方式
const deliveryMethodSaving = ref(false)
const deliveryMethodFileList = ref<CheckupFile[]>([])
const deliveryMethodForm = reactive({
deliveryMode: '' as string,
attachments: '' as string,
shipTime: '' as string,
handoverTemperature: '' as string,
pickupPerson: '' as string,
pickupPhone: '' as string,
senderName: '' as string,
expressCompany: '' as string,
expressNo: '' as string,
expressFee: null as number | null,
estimatedArrivalTime: '' as string
})
const loadDeliveryList = async () => {
if (!props.applicationId) return
deliveryLoading.value = true
try {
const res: any = await fetchPilotDeliveryListByApplication({
pilotApplicationId: props.applicationId
})
deliveryList.value = unwrapApiList(res)
} catch {
deliveryList.value = []
} finally {
deliveryLoading.value = false
}
}
const handleCreateDelivery = async (row: any) => {
try {
await ElMessageBox.confirm('确认创建交付?创建后将自动扣减渠道费用/份数。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await createPilotDelivery({ infusionScheduleId: row.scheduleId })
ElMessage.success('交付创建成功')
await loadDeliveryList()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '创建失败')
}
}
const handleDeleteDelivery = async (row: any) => {
try {
await deletePilotDelivery({ id: row.deliveryId })
ElMessage.success('删除成功')
await loadDeliveryList()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
const openDeliveryDetailDialog = async (row: any) => {
currentDeliveryRow.value = row
deliveryDetailTab.value = 'materials'
deliveryDetailDialogVisible.value = true
await loadDeliveryMaterials()
await loadDeliveryMethod()
}
const loadDeliveryMaterials = async () => {
if (!currentDeliveryRow.value?.deliveryId) return
deliveryMaterialsLoading.value = true
try {
const res: any = await fetchPilotDeliveryMaterials({
pilotDeliveryId: currentDeliveryRow.value.deliveryId
})
deliveryMaterials.value = unwrapApiList(res)
} catch {
deliveryMaterials.value = []
} finally {
deliveryMaterialsLoading.value = false
}
}
const openAddMaterialDialog = () => {
addMaterialForm.materialName = ''
addMaterialForm.quantity = 1
addMaterialForm.unitName = ''
addMaterialForm.remark = ''
addMaterialDialogVisible.value = true
}
const handleSaveMaterial = async () => {
if (!addMaterialForm.materialName) {
ElMessage.warning('请输入物料名称')
return
}
addMaterialSaving.value = true
try {
await savePilotDeliveryMaterials({
pilotDeliveryId: currentDeliveryRow.value.deliveryId,
items: [
{
materialName: addMaterialForm.materialName,
quantity: addMaterialForm.quantity,
unitName: addMaterialForm.unitName || undefined,
remark: addMaterialForm.remark || undefined
}
]
})
ElMessage.success('添加成功')
addMaterialDialogVisible.value = false
await loadDeliveryMaterials()
} catch (e: any) {
ElMessage.error(e?.message || '添加失败')
} finally {
addMaterialSaving.value = false
}
}
const handleDeleteMaterial = async (row: any) => {
try {
await savePilotDeliveryMaterials({
pilotDeliveryId: currentDeliveryRow.value.deliveryId,
deleteIds: [row.id]
})
ElMessage.success('删除成功')
await loadDeliveryMaterials()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ── 交付方式 ──────────────────────────────────────────────────────────
const loadDeliveryMethod = async () => {
if (!currentDeliveryRow.value?.deliveryId) return
try {
const res: any = await fetchPilotDeliveryMethod({
pilotDeliveryId: currentDeliveryRow.value.deliveryId
})
const data = res?.data ?? res
if (data && Object.keys(data).length > 0) {
deliveryMethodForm.deliveryMode = data.deliveryMode || ''
deliveryMethodForm.attachments = data.attachments || ''
deliveryMethodForm.shipTime = data.shipTime || ''
deliveryMethodForm.handoverTemperature = data.handoverTemperature || ''
deliveryMethodForm.pickupPerson = data.pickupPerson || ''
deliveryMethodForm.pickupPhone = data.pickupPhone || ''
deliveryMethodForm.senderName = data.senderName || ''
deliveryMethodForm.expressCompany = data.expressCompany || ''
deliveryMethodForm.expressNo = data.expressNo || ''
deliveryMethodForm.expressFee = data.expressFee ?? null
deliveryMethodForm.estimatedArrivalTime = data.estimatedArrivalTime || ''
deliveryMethodFileList.value = parseAttachmentUrls(deliveryMethodForm.attachments)
}
} catch {
// ignore
}
}
const handleDeliveryMethodFileChange = async (uploadFile: any) => {
if (!uploadFile?.raw) return
try {
const formData = new FormData()
formData.append('file', uploadFile.raw)
const res: any = await fetchUploadAttachment(formData)
const fileUrl = res?.data?.fileUrl || res?.fileUrl || res?.data?.url || ''
if (fileUrl) {
deliveryMethodFileList.value.push({ name: uploadFile.name, url: fileUrl })
}
} catch (e: any) {
ElMessage.error(e?.message || '上传失败')
}
}
const removeDeliveryMethodFile = (idx: number) => {
deliveryMethodFileList.value.splice(idx, 1)
}
const handleSaveDeliveryMethod = async () => {
deliveryMethodSaving.value = true
try {
const attachments = deliveryMethodFileList.value.map((f) => f.url).join(',')
await savePilotDeliveryMethod({
pilotDeliveryId: currentDeliveryRow.value.deliveryId,
deliveryMode: deliveryMethodForm.deliveryMode || undefined,
attachments: attachments || undefined,
shipTime: deliveryMethodForm.shipTime || undefined,
handoverTemperature: deliveryMethodForm.handoverTemperature || undefined,
pickupPerson: deliveryMethodForm.pickupPerson || undefined,
pickupPhone: deliveryMethodForm.pickupPhone || undefined,
senderName: deliveryMethodForm.senderName || undefined,
expressCompany: deliveryMethodForm.expressCompany || undefined,
expressNo: deliveryMethodForm.expressNo || undefined,
expressFee: deliveryMethodForm.expressFee ?? undefined,
estimatedArrivalTime: deliveryMethodForm.estimatedArrivalTime || undefined
})
ElMessage.success('保存成功')
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
deliveryMethodSaving.value = false
}
}
// ── 初始化 ────────────────────────────────────────────────────────────
const initData = async () => {
await loadApplicationDetail()
await loadPatient()
await loadPlans()
await loadCheckup()
if (checkupData.value?.id) {
await loadReserveItems()
await loadActualItems()
}
await loadDeliveryList()
}
watch(
() => [props.applicationId, props.modelValue, props.mode] as const,
([newId, visible, mode]) => {
if (!visible) return
if (mode === 'create') {
// 创建模式:加载渠道下拉
Object.assign(createForm, { channelId: '', channelServiceOrderId: '', remark: '' })
createServiceOrderOptions.value = []
localPlanList.value = []
selectedCustomerId.value = null
customerSnapshot.value = null
customerOptions.value = []
activeTab.value = 'basic'
void loadCreateChannels()
} else if (newId) {
// 查看模式:加载详情
initData()
}
},
{ immediate: true }
)
</script>
<style lang="scss" scoped>
/* ---- Dialog Header ---- */
.detail-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.detail-dialog-header-left {
display: flex;
align-items: center;
gap: 12px;
h3 {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
}
.detail-dialog-header-right {
display: flex;
align-items: center;
gap: 8px;
}
.detail-code {
font-size: 14px;
font-weight: 400;
color: var(--el-text-color-secondary);
}
/* ---- Dialog Body ---- */
.application-detail-body {
background: #f5f7fa;
overflow: visible;
min-height: 200px;
}
/* ---- Sticky Tab 导航 ---- */
.detail-tabs {
position: sticky;
top: 0;
z-index: 1000;
display: flex;
gap: 4px;
background: #f0fdf4;
padding: 4px;
border-radius: 8px;
margin: 0 28px 16px;
width: fit-content;
isolation: isolate;
box-shadow: 0 1px 0 #f0fdf4;
}
.detail-tab {
padding: 10px 20px;
cursor: pointer;
border-radius: 6px;
font-size: 14px;
color: #606266;
transition: all 0.3s;
&.active {
background: #fff;
color: #065f46;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&:hover:not(.active) {
color: #065f46;
}
}
/* ---- KPI 摘要条 ---- */
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
margin: 0 28px 24px;
padding: 6px;
background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
border: 1px solid #a8d4c4;
border-radius: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
}
.kpi-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
background: transparent;
border-radius: 8px;
transition: background 0.2s;
&:not(:last-child) {
border-right: 1px solid rgba(168, 212, 196, 0.75);
}
&:hover {
background: rgba(255, 255, 255, 0.55);
}
}
.kpi-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 10px;
background: #fff;
border: 1px solid #a8d4c4;
color: #065f46;
font-size: 18px;
}
.kpi-card__main {
min-width: 0;
flex: 1;
}
.kpi-label {
margin-bottom: 4px;
font-size: 11px;
font-weight: 500;
color: var(--mint-text-muted, #334155);
}
.kpi-value {
font-size: 15px;
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
&--emphasis {
font-size: 16px;
font-weight: 700;
color: #047857;
letter-spacing: 0.02em;
}
}
/* ---- Section Card ---- */
.section-card {
border-radius: 12px;
padding: 22px 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition:
transform 0.2s,
box-shadow 0.2s;
position: relative;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
&--white {
background: #fff;
}
&--green {
background: #f0fdf4;
}
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid #e2e8f0;
&::before {
content: '';
width: 3px;
height: 18px;
background: #047857;
border-radius: 2px;
margin-right: 10px;
flex-shrink: 0;
}
h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #1e293b;
line-height: 1.4;
}
}
/* ---- Data Grid ---- */
.data-grid {
display: grid;
gap: 14px 20px;
&--2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
&--3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
&--4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.data-item {
min-width: 0;
}
.data-label {
font-size: 12px;
color: var(--mint-text-muted, #334155);
margin-bottom: 3px;
}
.data-value {
font-size: 14px;
color: #1e293b;
font-weight: 500;
word-break: break-word;
}
/* ---- Form Tab Pane ---- */
.form-tab-pane {
padding: 0 28px 4px;
& + .form-tab-pane {
margin-top: 0;
padding-top: 0;
}
}
/* ---- Form Grid ---- */
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px 20px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.form-col-span-2 {
grid-column: 1 / -1;
}
/* ---- Legacy Detail Section (kept for compatibility) ---- */
.detail-section {
margin-bottom: 24px;
}
.detail-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.detail-section-title {
font-size: 15px;
font-weight: 600;
margin: 0 0 16px;
padding-left: 10px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
border-radius: 2px;
background-color: #047857;
}
}
.detail-section-header .detail-section-title {
margin-bottom: 0;
}
.detail-section-actions {
display: flex;
align-items: center;
gap: 8px;
}
.detail-section-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
/* ---- Expand / Collapse ---- */
.expand-content {
padding: 12px 48px 12px 24px;
}
.expand-footer {
margin-top: 12px;
padding-top: 8px;
}
/* ---- Customer Dialog ---- */
.customer-dialog-content {
.filter-form {
margin-bottom: 12px;
}
}
/* ---- Attachment ---- */
.attachment-upload-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
}
.attachment-card {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
border: 1px solid var(--el-border-color);
border-radius: 6px;
background: var(--el-fill-color-lighter);
max-width: 200px;
}
.attachment-icon {
color: var(--el-color-primary);
flex-shrink: 0;
}
.attachment-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
flex: 1;
min-width: 0;
}
.attachment-trigger {
display: inline-flex;
}
.attachment-trigger-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 60px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
color: var(--el-text-color-secondary);
font-size: 12px;
gap: 4px;
transition: border-color 0.2s;
&:hover {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
.form-item-hint {
font-size: 12px;
color: var(--el-text-color-placeholder);
margin-top: 4px;
}
.attachment-link {
cursor: pointer;
text-decoration: none;
color: inherit;
transition: border-color 0.2s;
&:hover {
border-color: var(--el-color-primary);
}
}
</style>