3818 lines
135 KiB
Vue
3818 lines
135 KiB
Vue
<!-- 中试服务申请单详情弹窗 -->
|
||
<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>
|