966 lines
30 KiB
Vue
966 lines
30 KiB
Vue
<!-- 体检管理 — 批量预约体检(选择患者 + 统一机构/时间/项目) -->
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="isFillFromListMode ? '批量预约体检 — 已选列表患者' : '批量预约体检 — 待登记患者'"
|
||
width="1380px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
>
|
||
<DialogFormBody :loading="submitLoading">
|
||
<!-- 患者选择区 -->
|
||
<div class="section-card" style="margin-bottom: 16px">
|
||
<div class="section-title section-title--mint-bar">
|
||
<h3>{{ isFillFromListMode ? '已选患者' : '选择患者' }}</h3>
|
||
</div>
|
||
<template v-if="isFillFromListMode">
|
||
<el-table
|
||
:data="listModePatients"
|
||
class="list-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="240"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="applicationNo"
|
||
label="申请单号"
|
||
min-width="160"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="patientName"
|
||
label="患者姓名"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="sex"
|
||
label="性别"
|
||
min-width="60"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ sexLabel(row.sex) || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="age"
|
||
label="年龄"
|
||
min-width="60"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.age ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="channelName"
|
||
label="渠道名称"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
</el-table>
|
||
<div style="margin-top: 8px; font-size: 13px; color: #64748b">
|
||
共
|
||
<strong style="color: #10b981">{{ listModePatients.length }}</strong>
|
||
名患者,将统一预约相同机构、时间与体检项目
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div style="display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap">
|
||
<el-input
|
||
v-model="patientSearch.applicationNo"
|
||
placeholder="申请单号"
|
||
clearable
|
||
style="width: 180px"
|
||
/>
|
||
<el-input
|
||
v-model="patientSearch.patientName"
|
||
placeholder="患者姓名"
|
||
clearable
|
||
style="width: 180px"
|
||
/>
|
||
<el-button class="btn-main" type="primary" @click="searchPatients">
|
||
<el-icon><Search /></el-icon>搜索
|
||
</el-button>
|
||
</div>
|
||
<el-table
|
||
:data="patientList"
|
||
v-loading="patientLoading"
|
||
class="list-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="240"
|
||
@selection-change="(rows) => (selectedPatients = rows)"
|
||
>
|
||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="applicationNo"
|
||
label="申请单号"
|
||
min-width="160"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="patientName"
|
||
label="患者姓名"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="sex"
|
||
label="性别"
|
||
min-width="60"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ sexLabel(row.sex) || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="age"
|
||
label="年龄"
|
||
min-width="60"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.age ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="channelName"
|
||
label="渠道名称"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
</el-table>
|
||
<div style="margin-top: 8px; font-size: 13px; color: #64748b">
|
||
已选
|
||
<strong style="color: #10b981">{{ selectedPatients.length }}</strong>
|
||
名患者(需体检且尚未登记体检记录)
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 体检信息区 -->
|
||
<div class="section-card" style="margin-bottom: 16px">
|
||
<div class="section-title section-title--mint-bar"><h3>体检信息</h3></div>
|
||
<div class="form-grid form-grid-2">
|
||
<el-form-item label="体检机构" required>
|
||
<el-select
|
||
v-model="formData.checkupInstitutionId"
|
||
placeholder="请选择体检机构"
|
||
clearable
|
||
style="width: 100%"
|
||
@change="onInstitutionChange"
|
||
>
|
||
<el-option
|
||
v-for="inst in institutionOptions"
|
||
:key="inst.id"
|
||
:label="inst.institutionName"
|
||
:value="inst.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="预约时间" required>
|
||
<el-date-picker
|
||
v-model="formData.reserveTime"
|
||
type="datetime"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
placeholder="请选择预约时间"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 项目选择区 -->
|
||
<div class="section-card checkup-items-section">
|
||
<div class="section-title section-title--mint-bar section-title--with-action">
|
||
<h3>体检项目</h3>
|
||
<div class="checkup-item-actions">
|
||
<el-button size="small" type="primary" @click="handleOpenProjectSelector">
|
||
<el-icon><Plus /></el-icon>添加单项
|
||
</el-button>
|
||
<el-button size="small" @click="handleOpenPackageSelector">
|
||
<el-icon><Suitcase /></el-icon>从套餐添加
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<CheckupReserveItemsTable
|
||
:items="formData.reserveItems"
|
||
@remove="(i) => formData.reserveItems.splice(i, 1)"
|
||
>
|
||
<template #total="{ total }">
|
||
单人预约费用合计:<strong>¥{{ formatMoneyDisplay(total) }}</strong>
|
||
<span v-if="patientCountForFee > 0" class="checkup-reserve-total-count">
|
||
× {{ patientCountForFee }} 人
|
||
</span>
|
||
</template>
|
||
</CheckupReserveItemsTable>
|
||
</div>
|
||
</DialogFormBody>
|
||
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||
<el-icon><Check /></el-icon>确定预约
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 单项:选择体检项目 -->
|
||
<el-dialog
|
||
v-model="projectSelectorVisible"
|
||
title="选择体检项目"
|
||
width="960px"
|
||
class="dialog-form-layout checkup-picker-dialog"
|
||
align-center
|
||
append-to-body
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="list-toolbar checkup-picker-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ projectSelectorSelected.length }}</span> 项
|
||
</div>
|
||
<el-input
|
||
v-model="projectSelectorKeyword"
|
||
placeholder="项目名称模糊检索"
|
||
clearable
|
||
class="checkup-picker-search"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="list-body list-wrap checkup-picker-list-wrap">
|
||
<el-table
|
||
:data="filteredProjectOptions"
|
||
v-loading="projectLoading"
|
||
class="list-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="620"
|
||
@selection-change="(rows) => (projectSelectorSelected = rows)"
|
||
>
|
||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="projectName"
|
||
label="项目名称"
|
||
min-width="180"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
label="项目费用(元)"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{
|
||
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="projectStatusTagType(row.status)" size="small">{{
|
||
projectStatusLabel(row.status)
|
||
}}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="remark"
|
||
label="备注"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
</el-table>
|
||
</div>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="projectSelectorVisible = false">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" @click="confirmProjectSelect">
|
||
<el-icon><Check /></el-icon>确认添加({{ projectSelectorSelected.length }})
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 套餐:选择套餐 -->
|
||
<el-dialog
|
||
v-model="packageSelectorVisible"
|
||
title="选择体检套餐"
|
||
width="1080px"
|
||
class="dialog-form-layout checkup-picker-dialog"
|
||
align-center
|
||
append-to-body
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="list-toolbar checkup-picker-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ packageSelectorSelected.length }}</span> 项
|
||
</div>
|
||
<el-input
|
||
v-model="packageSelectorKeyword"
|
||
placeholder="套餐名称模糊检索"
|
||
clearable
|
||
class="checkup-picker-search"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="list-body list-wrap checkup-picker-list-wrap">
|
||
<el-table
|
||
:data="filteredPackageOptions"
|
||
v-loading="packageLoading"
|
||
class="list-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="620"
|
||
@selection-change="(rows) => (packageSelectorSelected = rows)"
|
||
>
|
||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="packageName"
|
||
label="套餐名称"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="packageCode"
|
||
label="套餐编码"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="itemCount"
|
||
label="项目数"
|
||
min-width="80"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ row.itemCount ?? '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="合计费用(元)"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{
|
||
row.totalFee != null ? formatMoneyDisplay(row.totalFee) : '—'
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="projectStatusTagType(row.status)" size="small">{{
|
||
projectStatusLabel(row.status)
|
||
}}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="remark"
|
||
label="备注"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
label="操作"
|
||
min-width="100"
|
||
fixed="right"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-button class="btn-op-item" @click="handlePackageView(row)">
|
||
<el-icon><View /></el-icon>查看
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="packageSelectorVisible = false">
|
||
<el-icon><Close /></el-icon>取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="packageConfirmLoading"
|
||
@click="confirmPackageSelect"
|
||
>
|
||
<el-icon><Check /></el-icon>确认添加({{ packageSelectorSelected.length }})
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 套餐项目明细(查看) -->
|
||
<el-dialog
|
||
v-model="packageViewVisible"
|
||
:title="packageViewTitle"
|
||
width="900px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
append-to-body
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-table
|
||
:data="packageViewItems"
|
||
v-loading="packageViewLoading"
|
||
class="list-table"
|
||
stripe
|
||
max-height="420"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="projectName"
|
||
label="项目名称"
|
||
min-width="180"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">{{
|
||
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
|
||
}}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="projectStatusTagType(row.status)" size="small">{{
|
||
projectStatusLabel(row.status)
|
||
}}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="remark"
|
||
label="备注"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="packageViewVisible = false">
|
||
<el-icon><Close /></el-icon>关闭
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, watch, toRef } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Plus, Close, Check, Search, Delete, Suitcase, View } from '@element-plus/icons-vue'
|
||
import {
|
||
fetchPilotCheckupPendingPatients,
|
||
fetchCheckupInstitutionList,
|
||
fetchCheckupInstitutionProjects,
|
||
fetchCheckupInstitutionPackages,
|
||
fetchCheckupInstitutionPackageDetail,
|
||
batchSavePilotCheckup,
|
||
savePilotCheckup,
|
||
savePilotCheckupReserveItems
|
||
} from '@/api/admin'
|
||
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import { sexLabel } from '@/utils/status-tag-registry'
|
||
import CheckupReserveItemsTable from './CheckupReserveItemsTable.vue'
|
||
import {
|
||
type CheckupReserveItem,
|
||
appendCheckupReserveItems,
|
||
sumReserveItemsFee
|
||
} from './checkup-reserve-items'
|
||
|
||
type CheckupBatchRow = {
|
||
id: string
|
||
pilotPatientId?: string
|
||
patientName?: string
|
||
applicationNo?: string
|
||
sex?: string
|
||
age?: number | null
|
||
channelName?: string
|
||
}
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
/** 从列表勾选的待填写体检记录 */
|
||
initialCheckupRows?: CheckupBatchRow[]
|
||
}>()
|
||
const emit = defineEmits(['update:modelValue', 'success'])
|
||
|
||
const initialCheckupRows = toRef(props, 'initialCheckupRows')
|
||
const isFillFromListMode = computed(() => (initialCheckupRows.value?.length ?? 0) > 0)
|
||
const listModePatients = computed(() => initialCheckupRows.value ?? [])
|
||
|
||
function parseList(res: unknown): any[] {
|
||
return Array.isArray(res) ? res : (res as any)?.list || []
|
||
}
|
||
|
||
function projectStatusLabel(status: string | undefined) {
|
||
return status === '1' ? '正常' : '停用'
|
||
}
|
||
|
||
function projectStatusTagType(status: string | undefined) {
|
||
return status === '1' ? 'success' : 'info'
|
||
}
|
||
|
||
// ── 患者搜索 ──────────────────────────
|
||
const patientSearch = reactive({ applicationNo: '', patientName: '' })
|
||
const patientLoading = ref(false)
|
||
const patientList = ref<any[]>([])
|
||
const selectedPatients = ref<any[]>([])
|
||
|
||
const searchPatients = async () => {
|
||
patientLoading.value = true
|
||
try {
|
||
const res = await fetchPilotCheckupPendingPatients({
|
||
page: 1,
|
||
pageSize: 100,
|
||
applicationNo: patientSearch.applicationNo || undefined,
|
||
patientName: patientSearch.patientName || undefined
|
||
})
|
||
patientList.value = parseList(res)
|
||
} catch {
|
||
patientList.value = []
|
||
} finally {
|
||
patientLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ── 体检机构 ──────────────────────────
|
||
const institutionOptions = ref<any[]>([])
|
||
const loadInstitutions = async () => {
|
||
try {
|
||
const res = await fetchCheckupInstitutionList({ page: 1, pageSize: 100, status: '1' })
|
||
institutionOptions.value = parseList(res)
|
||
} catch {
|
||
institutionOptions.value = []
|
||
}
|
||
}
|
||
|
||
// ── 表单数据 ──────────────────────────
|
||
const formData = reactive({
|
||
checkupInstitutionId: '',
|
||
reserveTime: '',
|
||
reserveItems: [] as CheckupReserveItem[]
|
||
})
|
||
const submitLoading = ref(false)
|
||
|
||
const totalReserveFee = computed(() => sumReserveItemsFee(formData.reserveItems))
|
||
|
||
const patientCountForFee = computed(() =>
|
||
isFillFromListMode.value ? listModePatients.value.length : selectedPatients.value.length
|
||
)
|
||
|
||
// ── 机构变更 → 加载项目和套餐 ─────────
|
||
const projectOptions = ref<any[]>([])
|
||
const packageOptions = ref<any[]>([])
|
||
const projectLoading = ref(false)
|
||
const packageLoading = ref(false)
|
||
|
||
const loadInstitutionCatalog = async (instId: string) => {
|
||
projectLoading.value = true
|
||
packageLoading.value = true
|
||
try {
|
||
const [projRes, pkgRes] = await Promise.all([
|
||
fetchCheckupInstitutionProjects(instId),
|
||
fetchCheckupInstitutionPackages(instId)
|
||
])
|
||
projectOptions.value = parseList(projRes)
|
||
packageOptions.value = parseList(pkgRes)
|
||
} catch {
|
||
projectOptions.value = []
|
||
packageOptions.value = []
|
||
} finally {
|
||
projectLoading.value = false
|
||
packageLoading.value = false
|
||
}
|
||
}
|
||
|
||
const onInstitutionChange = async (instId: string) => {
|
||
formData.reserveItems = []
|
||
projectOptions.value = []
|
||
packageOptions.value = []
|
||
if (!instId) return
|
||
await loadInstitutionCatalog(instId)
|
||
}
|
||
|
||
const requireInstitution = () => {
|
||
if (!formData.checkupInstitutionId) {
|
||
ElMessage.warning('请先选择体检机构')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ── 单项选择 ──────────────────────────
|
||
const projectSelectorVisible = ref(false)
|
||
const projectSelectorKeyword = ref('')
|
||
const projectSelectorSelected = ref<any[]>([])
|
||
|
||
const filteredProjectOptions = computed(() => {
|
||
const kw = projectSelectorKeyword.value.trim().toLowerCase()
|
||
if (!kw) return projectOptions.value
|
||
return projectOptions.value.filter((row) => (row.projectName || '').toLowerCase().includes(kw))
|
||
})
|
||
|
||
const handleOpenProjectSelector = async () => {
|
||
if (!requireInstitution()) return
|
||
projectSelectorKeyword.value = ''
|
||
projectSelectorSelected.value = []
|
||
if (projectOptions.value.length === 0) {
|
||
await loadInstitutionCatalog(formData.checkupInstitutionId)
|
||
}
|
||
projectSelectorVisible.value = true
|
||
}
|
||
|
||
const confirmProjectSelect = () => {
|
||
if (projectSelectorSelected.value.length === 0) {
|
||
ElMessage.warning('请至少选择一个项目')
|
||
return
|
||
}
|
||
const added = appendCheckupReserveItems(formData.reserveItems, projectSelectorSelected.value)
|
||
projectSelectorVisible.value = false
|
||
if (added > 0) {
|
||
ElMessage.success(`已添加 ${added} 个项目`)
|
||
} else {
|
||
ElMessage.info('所选项目均已存在,未重复添加')
|
||
}
|
||
}
|
||
|
||
// ── 套餐选择 ──────────────────────────
|
||
const packageSelectorVisible = ref(false)
|
||
const packageSelectorKeyword = ref('')
|
||
const packageSelectorSelected = ref<any[]>([])
|
||
const packageConfirmLoading = ref(false)
|
||
|
||
const filteredPackageOptions = computed(() => {
|
||
const kw = packageSelectorKeyword.value.trim().toLowerCase()
|
||
if (!kw) return packageOptions.value
|
||
return packageOptions.value.filter((row) => (row.packageName || '').toLowerCase().includes(kw))
|
||
})
|
||
|
||
const handleOpenPackageSelector = async () => {
|
||
if (!requireInstitution()) return
|
||
packageSelectorKeyword.value = ''
|
||
packageSelectorSelected.value = []
|
||
if (packageOptions.value.length === 0) {
|
||
await loadInstitutionCatalog(formData.checkupInstitutionId)
|
||
}
|
||
packageSelectorVisible.value = true
|
||
}
|
||
|
||
const confirmPackageSelect = async () => {
|
||
if (packageSelectorSelected.value.length === 0) {
|
||
ElMessage.warning('请至少选择一个套餐')
|
||
return
|
||
}
|
||
packageConfirmLoading.value = true
|
||
try {
|
||
const allItems: any[] = []
|
||
for (const pkg of packageSelectorSelected.value) {
|
||
const detail: any = await fetchCheckupInstitutionPackageDetail(pkg.id)
|
||
const items = detail?.items ?? detail?.detailItems ?? []
|
||
allItems.push(...items)
|
||
}
|
||
const added = appendCheckupReserveItems(formData.reserveItems, allItems)
|
||
packageSelectorVisible.value = false
|
||
if (added > 0) {
|
||
ElMessage.success(`已从套餐添加 ${added} 个项目`)
|
||
} else {
|
||
ElMessage.info('套餐中的项目均已存在,未重复添加')
|
||
}
|
||
} catch {
|
||
ElMessage.error('加载套餐项目失败')
|
||
} finally {
|
||
packageConfirmLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ── 套餐明细查看 ──────────────────────
|
||
const packageViewVisible = ref(false)
|
||
const packageViewTitle = ref('')
|
||
const packageViewItems = ref<any[]>([])
|
||
const packageViewLoading = ref(false)
|
||
|
||
const handlePackageView = async (row: any) => {
|
||
packageViewTitle.value = `套餐项目 — ${row.packageName || ''}`
|
||
packageViewItems.value = []
|
||
packageViewVisible.value = true
|
||
packageViewLoading.value = true
|
||
try {
|
||
const detail: any = await fetchCheckupInstitutionPackageDetail(row.id)
|
||
packageViewItems.value = detail?.items ?? detail?.detailItems ?? []
|
||
} catch {
|
||
ElMessage.error('加载套餐明细失败')
|
||
packageViewVisible.value = false
|
||
} finally {
|
||
packageViewLoading.value = false
|
||
}
|
||
}
|
||
|
||
// ── 提交 ──────────────────────────────
|
||
const reserveItemsPayload = () =>
|
||
formData.reserveItems.map((item) => ({
|
||
projectName: item.projectName,
|
||
projectFee: item.projectFee
|
||
}))
|
||
|
||
const handleSubmit = async () => {
|
||
if (isFillFromListMode.value) {
|
||
if (listModePatients.value.length === 0) {
|
||
ElMessage.warning('未选择患者')
|
||
return
|
||
}
|
||
} else if (selectedPatients.value.length === 0) {
|
||
ElMessage.warning('请至少选择一名患者')
|
||
return
|
||
}
|
||
if (!formData.checkupInstitutionId) {
|
||
ElMessage.warning('请选择体检机构')
|
||
return
|
||
}
|
||
if (!formData.reserveTime) {
|
||
ElMessage.warning('请选择预约时间')
|
||
return
|
||
}
|
||
if (formData.reserveItems.length === 0) {
|
||
ElMessage.warning('请至少添加一个体检项目')
|
||
return
|
||
}
|
||
|
||
submitLoading.value = true
|
||
try {
|
||
if (isFillFromListMode.value) {
|
||
const rows = listModePatients.value
|
||
for (const row of rows) {
|
||
await savePilotCheckup({
|
||
checkupId: row.id,
|
||
checkupInstitutionId: formData.checkupInstitutionId,
|
||
reserveTime: formData.reserveTime,
|
||
reserveFee: totalReserveFee.value || undefined
|
||
})
|
||
await savePilotCheckupReserveItems({
|
||
pilotCheckupId: row.id,
|
||
items: reserveItemsPayload()
|
||
})
|
||
}
|
||
ElMessage.success(`成功为 ${rows.length} 名患者预约体检`)
|
||
} else {
|
||
const patientIds = selectedPatients.value.map((p) => p.id)
|
||
await batchSavePilotCheckup({
|
||
patientIds,
|
||
checkupInstitutionId: formData.checkupInstitutionId,
|
||
reserveTime: formData.reserveTime,
|
||
reserveItems: reserveItemsPayload()
|
||
})
|
||
ElMessage.success(`成功为 ${patientIds.length} 名患者预约体检`)
|
||
}
|
||
emit('update:modelValue', false)
|
||
emit('success')
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.message || '预约失败')
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(val) => {
|
||
if (val) {
|
||
patientSearch.applicationNo = ''
|
||
patientSearch.patientName = ''
|
||
selectedPatients.value = []
|
||
formData.checkupInstitutionId = ''
|
||
formData.reserveTime = ''
|
||
formData.reserveItems = []
|
||
projectOptions.value = []
|
||
packageOptions.value = []
|
||
if (isFillFromListMode.value) {
|
||
selectedPatients.value = []
|
||
} else {
|
||
searchPatients()
|
||
}
|
||
loadInstitutions()
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.section-title--mint-bar {
|
||
margin-bottom: 12px;
|
||
|
||
h3 {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
line-height: 1.4;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 3px;
|
||
height: 18px;
|
||
background: var(--primary, #10b981);
|
||
border-radius: 2px;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.checkup-items-section {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.section-title--with-action {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.checkup-item-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.checkup-reserve-total-count {
|
||
margin-left: 16px;
|
||
color: #64748b;
|
||
}
|
||
|
||
:deep(.checkup-picker-dialog.el-dialog.dialog-form-layout) {
|
||
.el-dialog__body {
|
||
min-height: min(680px, calc(100vh - 160px));
|
||
padding-top: 16px;
|
||
}
|
||
}
|
||
|
||
.checkup-picker-toolbar {
|
||
margin-bottom: 0;
|
||
background: var(--mint-table-head, #f8fafc);
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-bottom: none;
|
||
border-radius: var(--mint-radius, 10px) var(--mint-radius, 10px) 0 0;
|
||
|
||
.list-toolbar-row {
|
||
padding: 8px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.list-toolbar-selected {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-height: 36px;
|
||
padding: 0 10px 0 0;
|
||
margin-right: 4px;
|
||
border-right: 1px solid var(--mint-border, #e2e8f0);
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #64748b);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sel-num {
|
||
color: var(--mint-primary-dark, #059669);
|
||
font-weight: 700;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
.checkup-picker-search {
|
||
width: 280px;
|
||
}
|
||
}
|
||
|
||
.checkup-picker-list-wrap.list-body.list-wrap {
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: 0 0 var(--mint-radius, 10px) var(--mint-radius, 10px);
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
:deep(.el-table) {
|
||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||
--el-table-text-color: var(--mint-text, #0f172a);
|
||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.el-table th.el-table__cell) {
|
||
color: var(--mint-text-muted, #64748b);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.el-table td.el-table__cell) {
|
||
color: var(--mint-text, #0f172a);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
</style>
|