3984 lines
149 KiB
Vue
3984 lines
149 KiB
Vue
<!-- 处方管理 -->
|
||
<template>
|
||
<div class="prescription-list">
|
||
<el-card class="filter-toolbar" shadow="never" :body-style="{ padding: '10px 14px' }">
|
||
<div class="pl-toolbar">
|
||
<div class="pl-toolbar__line">
|
||
<span class="pl-toolbar__k" title="按处方创建时间">创建时间</span>
|
||
<el-radio-group
|
||
v-model="dateQuickTab"
|
||
size="small"
|
||
class="pl-toolbar__rg"
|
||
@change="onDateQuickTabChange"
|
||
>
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="today">今日</el-radio-button>
|
||
<el-radio-button label="yesterday">昨日</el-radio-button>
|
||
<el-radio-button label="before_yesterday">前天</el-radio-button>
|
||
</el-radio-group>
|
||
<DaterangePicker
|
||
class="pl-toolbar__range"
|
||
v-model:startTime="formData.start_time"
|
||
v-model:endTime="formData.end_time"
|
||
/>
|
||
<span class="pl-toolbar__sep" aria-hidden="true" />
|
||
<span
|
||
class="pl-toolbar__k"
|
||
title="「未通过」含待审核与已驳回"
|
||
>审核</span>
|
||
<el-radio-group
|
||
v-model="formData.audit_filter"
|
||
size="small"
|
||
class="pl-toolbar__rg pl-toolbar__rg--audit"
|
||
@change="onAuditFilterChange"
|
||
>
|
||
<el-radio-button label="pending" class="pl-audit-pending-btn">待审核</el-radio-button>
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="passed">已通过</el-radio-button>
|
||
<el-radio-button label="not_passed">未通过</el-radio-button>
|
||
<el-radio-button label="rejected">驳回</el-radio-button>
|
||
</el-radio-group>
|
||
<span class="pl-toolbar__sep" aria-hidden="true" />
|
||
<span class="pl-toolbar__k" title="与列表「来源」列一致:系统代开 / 手工">来源</span>
|
||
<el-radio-group
|
||
v-model="formData.source_filter"
|
||
size="small"
|
||
class="pl-toolbar__rg pl-toolbar__rg--source"
|
||
@change="onSourceFilterChange"
|
||
>
|
||
<el-radio-button label="all">全部</el-radio-button>
|
||
<el-radio-button label="manual">手工</el-radio-button>
|
||
<el-radio-button label="system">系统代开</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<div class="pl-toolbar__line pl-toolbar__line--second">
|
||
<el-form :model="formData" inline class="pl-toolbar__form">
|
||
<el-form-item label="处方编号" class="!mb-0">
|
||
<el-input
|
||
v-model="formData.sn"
|
||
placeholder="编号"
|
||
clearable
|
||
class="pl-toolbar__input"
|
||
@keyup.enter="resetPage"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="患者" class="!mb-0">
|
||
<el-input
|
||
v-model="formData.patient_name"
|
||
placeholder="姓名"
|
||
clearable
|
||
class="pl-toolbar__input"
|
||
@keyup.enter="resetPage"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="医师" class="!mb-0">
|
||
<el-select
|
||
v-model="formData.creator_ids"
|
||
multiple
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
filterable
|
||
clearable
|
||
placeholder="全部医师"
|
||
class="pl-toolbar__select pl-toolbar__select--doctors"
|
||
>
|
||
<el-option
|
||
v-for="d in doctorOptions"
|
||
:key="d.id"
|
||
:label="d.name"
|
||
:value="Number(d.id)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="pl-toolbar__actions">
|
||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||
<el-button @click="resetParams">重置</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card v-loading="pager.loading" class="mt-3 table-card" shadow="never">
|
||
<div class="pl-table-head">
|
||
<span class="pl-table-head__title">处方列表</span>
|
||
<div class="flex items-center gap-2">
|
||
<el-button
|
||
v-perms="['tcm.prescriptionOrder/lists']"
|
||
@click="goBusinessOrderList"
|
||
>
|
||
业务订单
|
||
</el-button>
|
||
<el-button type="primary" @click="handleAdd" v-perms="['cf.prescription/add']">
|
||
<template #icon>
|
||
<icon name="el-icon-Plus" />
|
||
</template>
|
||
新增处方
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<el-table :data="pager.lists" size="default" stripe>
|
||
<el-table-column label="处方编号" min-width="160">
|
||
<template #default="{ row }">
|
||
<span class="font-medium">{{ row.sn || '—' }}</span>
|
||
<div class="text-xs text-gray-400 mt-0.5">ID: {{ row.id }}</div>
|
||
<div
|
||
v-for="(warn, idx) in listRxOrderWarnings(row)"
|
||
:key="`rx-order-warn-${row.id}-${idx}`"
|
||
class="text-xs text-red-600 mt-0.5 leading-snug"
|
||
>
|
||
{{ warn }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="处方类型" min-width="100">
|
||
<template #default="{ row }">
|
||
<el-tag type="info" size="small">{{ row.prescription_type || '—' }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="来源" min-width="92" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
v-if="Number(row.is_system_auto) === 1"
|
||
type="warning"
|
||
size="small"
|
||
effect="plain"
|
||
>
|
||
系统代开
|
||
</el-tag>
|
||
<span v-else class="text-gray-400 text-sm">手工</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="患者信息" min-width="110">
|
||
<template #default="{ row }">
|
||
<div class="font-medium">{{ row.patient_name || '—' }}</div>
|
||
<div class="text-xs text-gray-500 mt-0.5">
|
||
{{ row.gender === 1 ? '男' : (row.gender === 0 ? '女' : '未知') }} · {{ row.age ? row.age + '岁' : '—' }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
|
||
|
||
<el-table-column label="审核状态" min-width="200">
|
||
<template #default="{ row }">
|
||
<div class="flex flex-col gap-1">
|
||
<div class="flex flex-wrap items-center gap-1">
|
||
<el-tag :type="listAuditStatusTagType(row)">
|
||
{{ listAuditStatusLabel(row) }}
|
||
</el-tag>
|
||
<span
|
||
v-if="Number(row.business_prescription_audit_rejected) === 1"
|
||
class="text-xs text-gray-500"
|
||
>
|
||
(业务订单审核)
|
||
</span>
|
||
</div>
|
||
<template v-if="listRejectReasonLines(row).length">
|
||
<div
|
||
v-for="(line, idx) in listRejectReasonLines(row)"
|
||
:key="idx"
|
||
class="text-xs text-red-600 leading-snug line-clamp-3"
|
||
:title="line"
|
||
>
|
||
{{ line }}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="作废" min-width="80" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.void_status === 1" type="danger" size="small">作废</el-tag>
|
||
<span v-else class="text-gray-400">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="医师信息" min-width="120">
|
||
<template #default="{ row }">
|
||
<div class="font-medium">{{ row.doctor_name || '—' }}</div>
|
||
<div class="text-xs text-gray-500 mt-0.5">{{ row.prescription_date || '—' }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="医助" min-width="90">
|
||
<template #default="{ row }">
|
||
<div class="text-sm">{{ row.assistant_name || '—' }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" min-width="160">
|
||
<template #default="{ row }">
|
||
{{ formatListCreateTime(row.create_time) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="300" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="handleView(row)" v-perms="['cf.prescription/read']">
|
||
查看
|
||
</el-button>
|
||
<el-button
|
||
v-if="!Number(row.has_prescription_order) && Number(row.void_status) !== 1"
|
||
type="success"
|
||
link
|
||
v-perms="['tcm.prescriptionOrder/create']"
|
||
@click="openCreateOrderFromPrescription(row)"
|
||
>
|
||
创建订单
|
||
</el-button>
|
||
<el-button
|
||
v-if="!isApprovedActivePrescription(row)"
|
||
type="primary"
|
||
link
|
||
@click="handleEdit(row)"
|
||
v-perms="['cf.prescription/edit']"
|
||
>
|
||
编辑
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.audit_status === 0 && row.void_status !== 1"
|
||
type="warning"
|
||
link
|
||
v-perms="['cf.prescription/audit']"
|
||
@click="openAuditDialog(row)"
|
||
>
|
||
审核
|
||
</el-button>
|
||
<el-button
|
||
v-if="!isApprovedActivePrescription(row)"
|
||
type="danger"
|
||
link
|
||
@click="handleDelete(row.id)"
|
||
v-perms="['cf.prescription/del']"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<div class="flex mt-2 justify-end">
|
||
<pagination v-model="pager" @change="getLists" />
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 新增/编辑/查看抽屉 -->
|
||
<el-drawer
|
||
v-model="showEdit"
|
||
:title="editMode === 'add' ? '新增处方' : editMode === 'edit' ? '编辑处方' : '查看处方'"
|
||
:size="editMode === 'view' ? '920px' : '860px'"
|
||
class="cf-prescription-drawer"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
@closed="onEditDialogClosed"
|
||
>
|
||
<!-- 查看:A4 处方笺(药房联样式) -->
|
||
<div v-if="editMode === 'view'" v-loading="viewDetailLoading" class="rx-wrap">
|
||
<!-- 顶部工具栏(不打印 / 不下载) -->
|
||
<div class="rx-toolbar">
|
||
<el-tag
|
||
v-if="slipView && Number(slipView.is_system_auto) === 1"
|
||
type="warning"
|
||
size="small"
|
||
effect="plain"
|
||
>系统代开</el-tag>
|
||
<el-tag v-if="slipView && Number(slipView.void_status) === 1" type="danger" size="small">已作废</el-tag>
|
||
<el-tag
|
||
v-else-if="slipView && Number(slipView.business_prescription_audit_rejected) === 1"
|
||
type="danger"
|
||
size="small"
|
||
>业务订单已驳回</el-tag>
|
||
<el-tag
|
||
v-else-if="slipView && Number(slipView.audit_status) === 1"
|
||
type="success"
|
||
size="small"
|
||
>已通过</el-tag>
|
||
<el-tag
|
||
v-else-if="slipView && Number(slipView.audit_status) === 0"
|
||
type="warning"
|
||
size="small"
|
||
>待审核</el-tag>
|
||
<el-tag v-else-if="slipView" type="danger" size="small">已驳回</el-tag>
|
||
<div class="flex-1" />
|
||
<el-button :icon="Printer" size="small" @click="handlePrintSlip">打印</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:icon="Download"
|
||
size="small"
|
||
:loading="downloadingSlip"
|
||
@click="handleDownloadSlip"
|
||
>
|
||
下载 PDF (A4)
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 状态提示(不进入打印区) -->
|
||
<div v-if="slipView && Number(slipView.void_status) === 1" class="rx-status rx-status-danger">
|
||
作废人:{{ slipView.void_by_name || '—' }},作废时间:{{ formatVoidTime(slipView.void_time) }}
|
||
</div>
|
||
<div
|
||
v-else-if="slipView && Number(slipView.business_prescription_audit_rejected) === 1"
|
||
class="rx-status rx-status-danger"
|
||
>
|
||
<div class="font-medium">审核状态:已驳回(业务订单·处方审核)</div>
|
||
<div v-if="slipView.business_prescription_audit_remark" class="mt-1">
|
||
驳回意见:{{ slipView.business_prescription_audit_remark }}
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-else-if="slipView && Number(slipView.audit_status) === 2"
|
||
class="rx-status rx-status-danger"
|
||
>
|
||
<div class="font-medium">消费者处方审核:已驳回</div>
|
||
<div v-if="slipView.audit_remark" class="mt-1">驳回意见:{{ slipView.audit_remark }}</div>
|
||
</div>
|
||
|
||
<!-- A4 处方纸 -->
|
||
<div v-if="slipView" ref="rxPaperRef" class="rx-paper">
|
||
<!-- 顶部告知栏 -->
|
||
<div class="rx-notice">
|
||
<span class="rx-notice-text">
|
||
服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点
|
||
</span>
|
||
<span class="rx-notice-meta">
|
||
<span>日期:{{ rxDateText }}</span>
|
||
<span>编号:{{ rxSerialText }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 患者信息表 -->
|
||
<div class="rx-info">
|
||
<div class="rx-info-row">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">姓名</span>
|
||
<span class="rx-val">{{ slipView.patient_name || '—' }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">性别</span>
|
||
<span class="rx-val">{{ slipGenderLabel(slipView.gender) }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">年龄</span>
|
||
<span class="rx-val">{{ slipAgeText(slipView.age) }}</span>
|
||
</div>
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">电话</span>
|
||
<span class="rx-val">{{ slipView.phone || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="rx-info-row rx-info-row-full">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">收件信息</span>
|
||
<span class="rx-val">{{ rxRecipientText }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="rx-info-row rx-info-row-full">
|
||
<div class="rx-info-cell">
|
||
<span class="rx-key">临床诊断</span>
|
||
<span class="rx-val">{{ slipView.clinical_diagnosis || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rp 区域(带水印) -->
|
||
<div class="rx-rp">
|
||
<div class="rx-watermark">药房联</div>
|
||
<div class="rx-rp-head">
|
||
<div class="rx-rp-label">Rp.</div>
|
||
<div class="rx-rp-cols-head">
|
||
<div class="rx-rp-col-head">
|
||
<span>用药 (单剂)</span>
|
||
<span>总量</span>
|
||
</div>
|
||
<div class="rx-rp-col-head">
|
||
<span>用药 (单剂)</span>
|
||
<span>总量</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rx-herbs">
|
||
<div
|
||
v-for="(h, i) in slipHerbsList"
|
||
:key="i"
|
||
class="rx-herb-cell"
|
||
>
|
||
<span class="rx-herb-name">{{ h.name }} ({{ h.dosage }}克)</span>
|
||
<span class="rx-herb-total">{{ rxHerbTotal(h.dosage) }}克</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服法 / 医嘱 / 备注 / 药房备注 / 出丸 -->
|
||
<div class="rx-text">
|
||
<p>服法:{{ rxUsageText }}</p>
|
||
<p v-if="rxAdviceText">医嘱:{{ rxAdviceText }}</p>
|
||
<p v-if="rxRemarkText">备注:{{ rxRemarkText }}</p>
|
||
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
|
||
药房备注:{{ rxPharmacyRemarkText }}
|
||
</p>
|
||
<p v-if="rxOutPelletText" class="rx-text-warn">
|
||
出丸:{{ rxOutPelletText }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 底部表:医师/审方/配药/复核/发药 + 类型/剂数/单剂量 -->
|
||
<div class="rx-bottom">
|
||
<div class="rx-bot-row rx-bot-row-1">
|
||
<div class="rx-bot-cell rx-bot-stack rx-bot-doctor">
|
||
<div class="rx-bot-label">医师</div>
|
||
<div class="rx-bot-doctor-body">
|
||
<img
|
||
v-if="slipView.doctor_signature"
|
||
:src="slipView.doctor_signature"
|
||
alt="医师签名"
|
||
class="rx-bot-sign-img"
|
||
/>
|
||
<div class="rx-bot-doctor-name">{{ slipView.doctor_name || '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">审方</div></div>
|
||
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">配药</div></div>
|
||
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">复核</div></div>
|
||
<div class="rx-bot-cell rx-bot-stack"><div class="rx-bot-label">发药</div></div>
|
||
<div class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">类型:</span>
|
||
<span class="rx-bot-meta-val">{{ rxTypeText }}</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">剂数:</span>
|
||
<span class="rx-bot-meta-val">{{ slipView.dose_count || '—' }}剂</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-bot-meta">
|
||
<span class="rx-bot-meta-key">单剂量:</span>
|
||
<span class="rx-bot-meta-val">{{ rxPerDoseAmount }}克</span>
|
||
</div>
|
||
</div>
|
||
<div class="rx-bot-row rx-bot-row-2">
|
||
<div class="rx-bot-cell rx-fee">
|
||
<span class="rx-fee-key">医药费</span>
|
||
<span class="rx-fee-val">{{ rxFee.medicine }}元</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-fee">
|
||
<span class="rx-fee-key">诊金:</span>
|
||
<span class="rx-fee-val">{{ rxFee.consult }}元</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-fee">
|
||
<span class="rx-fee-key">制作费:</span>
|
||
<span class="rx-fee-val">{{ rxFee.making }}元</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-fee">
|
||
<span class="rx-fee-key">运费:</span>
|
||
<span class="rx-fee-val">{{ rxFee.shipping }}元</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-fee">
|
||
<span class="rx-fee-key">优惠:</span>
|
||
<span class="rx-fee-val">{{ rxFee.discount }}元</span>
|
||
</div>
|
||
<div class="rx-bot-cell rx-fee rx-fee-total">
|
||
<span class="rx-fee-key">合计:</span>
|
||
<span class="rx-fee-val">{{ rxFee.total }}元</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 审核痕迹(不进入打印区) -->
|
||
<div
|
||
v-if="slipView && (slipView.audit_by_name || slipView.audit_remark)"
|
||
class="rx-audit"
|
||
>
|
||
<div v-if="slipView.audit_by_name">
|
||
审核人:{{ slipView.audit_by_name }}
|
||
<span v-if="slipView.audit_time" class="ml-2 text-gray-400">
|
||
{{ formatAuditTime(slipView.audit_time) }}
|
||
</span>
|
||
</div>
|
||
<div v-if="slipView.audit_remark" class="mt-1">审核意见:{{ slipView.audit_remark }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form
|
||
v-else
|
||
ref="formRef"
|
||
:model="editForm"
|
||
:rules="rules"
|
||
label-width="120px"
|
||
>
|
||
<el-alert
|
||
v-if="editMode === 'edit' && Number(editForm.is_system_auto) === 1"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
title="本处方为系统自动生成(如挂号完成),默认无药材配方,请补充药材与诊断等信息后再保存。"
|
||
/>
|
||
<el-alert
|
||
v-if="editMode === 'edit'"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
title="保存修改后,该处方将重新进入「待审核」,需审核人员再次通过后方可生效。"
|
||
/>
|
||
<el-alert
|
||
v-if="editMode === 'edit' && Number(editForm.audit_status) === 2"
|
||
type="error"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
>
|
||
<template #title>当前消费者处方为「已驳回」</template>
|
||
<div class="text-sm mt-1">
|
||
保存修改后将清除驳回并变为「待审核」。{{
|
||
editForm.audit_remark ? `上次意见:${editForm.audit_remark}` : ''
|
||
}}
|
||
</div>
|
||
</el-alert>
|
||
<el-alert
|
||
v-if="
|
||
editMode === 'edit' &&
|
||
Number(editForm.business_prescription_audit_rejected) === 1 &&
|
||
Number(editForm.audit_status) === 1
|
||
"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
>
|
||
<template #title>业务订单侧「处方审核」已驳回</template>
|
||
<div class="text-sm mt-1">
|
||
消费者处方仍为「已通过」;您修改并保存后,将重置关联业务订单的处方/支付单审核为「待审」,请在「处方业务订单」中继续处理。
|
||
</div>
|
||
<div
|
||
v-if="String(editForm.business_prescription_audit_remark || '').trim()"
|
||
class="text-sm mt-2 text-gray-800 font-medium"
|
||
>
|
||
驳回意见:{{ editForm.business_prescription_audit_remark }}
|
||
</div>
|
||
</el-alert>
|
||
<el-alert
|
||
v-if="editMode === 'edit' && editReviveHint"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb-4"
|
||
:title="editReviveHint"
|
||
/>
|
||
<div
|
||
v-if="Number(editForm.diagnosis_id) > 0"
|
||
class="mb-3 flex flex-wrap items-center gap-2 pl-[120px]"
|
||
>
|
||
<el-button type="primary" size="small" @click="openDiagnosisPatientDetail">
|
||
查看患者诊单详情
|
||
</el-button>
|
||
<span class="text-xs text-gray-400">关联诊单 #{{ editForm.diagnosis_id }}</span>
|
||
</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="患者姓名" prop="patient_name">
|
||
<el-input v-model="editForm.patient_name" placeholder="请输入患者姓名" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="门诊号" prop="visit_no">
|
||
<el-input v-model="editForm.visit_no" placeholder="自动生成或手动输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<el-form-item label="性别" prop="gender">
|
||
<el-radio-group v-model="editForm.gender">
|
||
<el-radio :label="1">男</el-radio>
|
||
<el-radio :label="0">女</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="年龄" prop="age">
|
||
<el-input-number
|
||
v-model="editForm.age"
|
||
:min="0"
|
||
:max="150"
|
||
placeholder="请输入年龄"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="处方日期" prop="prescription_date">
|
||
<el-date-picker
|
||
v-model="editForm.prescription_date"
|
||
type="date"
|
||
placeholder="选择日期"
|
||
format="YYYY-MM-DD"
|
||
value-format="YYYY-MM-DD"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="面象" prop="tongue">
|
||
<el-input v-model="editForm.tongue" placeholder="请输入面象" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="舌象" prop="tongue_image">
|
||
<el-input v-model="editForm.tongue_image" placeholder="请输入舌象" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="脉象" prop="pulse">
|
||
<el-input v-model="editForm.pulse" placeholder="请输入脉象" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="脉象详情" prop="pulse_condition">
|
||
<el-input v-model="editForm.pulse_condition" placeholder="请输入脉象详情" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="临床诊断" prop="clinical_diagnosis">
|
||
<el-input
|
||
v-model="editForm.clinical_diagnosis"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入临床诊断"
|
||
maxlength="500"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="药材配方" prop="herbs" required>
|
||
<div class="w-full">
|
||
<el-button type="primary" size="small" @click="addHerb">
|
||
添加药材
|
||
</el-button>
|
||
<el-button type="success" size="small" class="ml-2" @click="openLibraryDialog">
|
||
从处方库导入
|
||
</el-button>
|
||
<el-table :data="editForm.herbs" class="mt-2" border>
|
||
<el-table-column label="序号" type="index" width="60" />
|
||
<el-table-column label="药材名称" min-width="220">
|
||
<template #default="{ row }">
|
||
<MedicineNameSelect v-model="row.name" class="w-full" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="剂量(克)" min-width="120">
|
||
<template #default="{ row, $index }">
|
||
<el-input-number
|
||
v-model="row.dosage"
|
||
:min="0"
|
||
:precision="1"
|
||
:step="0.5"
|
||
placeholder="剂量"
|
||
class="w-full"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80">
|
||
<template #default="{ $index }">
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
@click="removeHerb($index)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
</el-table>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<el-form-item label="剂数" prop="dose_count">
|
||
<el-input-number
|
||
v-model="editForm.dose_count"
|
||
:min="1"
|
||
placeholder="剂数"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="剂量单位" prop="dose_unit">
|
||
<el-select v-model="editForm.dose_unit" placeholder="请选择" class="w-full">
|
||
<el-option label="剂" value="剂" />
|
||
<el-option label="丸" value="丸" />
|
||
<el-option label="袋" value="袋" />
|
||
<el-option label="盒" value="盒" />
|
||
<el-option label="瓶" value="瓶" />
|
||
<el-option label="膏" value="膏" />
|
||
<el-option label="贴" value="贴" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="处方类型" prop="prescription_type">
|
||
<el-select v-model="editForm.prescription_type" placeholder="请选择处方类型" class="w-full">
|
||
<el-option label="浓缩水丸" value="浓缩水丸" />
|
||
<el-option label="饮片" value="饮片" />
|
||
<el-option label="颗粒" value="颗粒" />
|
||
<el-option label="丸剂" value="丸剂" />
|
||
<el-option label="散剂" value="散剂" />
|
||
<el-option label="膏方" value="膏方" />
|
||
<el-option label="汤剂" value="汤剂" />
|
||
</el-select>
|
||
</el-form-item> </el-col>
|
||
|
||
<!-- 用量字段 -->
|
||
<el-col :span="8">
|
||
<el-form-item label="用量" prop="dosage_amount">
|
||
<!-- 浓缩水丸:1-10g 下拉选择 -->
|
||
<el-select
|
||
v-if="editForm.prescription_type === '浓缩水丸'"
|
||
v-model="editForm.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="w-full"
|
||
>
|
||
<el-option :label="1 + 'g'" :value="1" />
|
||
<el-option :label="2 + 'g'" :value="2" />
|
||
<el-option :label="3 + 'g'" :value="3" />
|
||
<el-option :label="4 + 'g'" :value="4" />
|
||
<el-option :label="5 + 'g'" :value="5" />
|
||
<el-option :label="6 + 'g'" :value="6" />
|
||
<el-option :label="7 + 'g'" :value="7" />
|
||
<el-option :label="8 + 'g'" :value="8" />
|
||
<el-option :label="9 + 'g'" :value="9" />
|
||
<el-option :label="10 + 'g'" :value="10" />
|
||
</el-select>
|
||
<!-- 饮片:50-250ml 下拉选择,步进50 -->
|
||
<el-select
|
||
v-else-if="editForm.prescription_type === '饮片'"
|
||
v-model="editForm.dosage_amount"
|
||
placeholder="请选择用量"
|
||
class="w-full"
|
||
>
|
||
<el-option label="50ml" :value="50" />
|
||
<el-option label="100ml" :value="100" />
|
||
<el-option label="120ml" :value="120" />
|
||
<el-option label="150ml" :value="150" />
|
||
<el-option label="180ml" :value="180" />
|
||
<el-option label="200ml" :value="200" />
|
||
<el-option label="250ml" :value="250" />
|
||
</el-select>
|
||
<!-- 其他类型:自由输入 -->
|
||
<el-input-number
|
||
v-else
|
||
v-model="editForm.dosage_amount"
|
||
:min="0"
|
||
:precision="2"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<!-- 饮片代煎选项 -->
|
||
<el-col v-if="editForm.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="是否代煎" prop="need_decoction">
|
||
<el-radio-group v-model="editForm.need_decoction">
|
||
<el-radio :label="true">代煎</el-radio>
|
||
<el-radio :label="false">不代煎</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<!-- 饮片每贴出包数 -->
|
||
<el-col v-if="editForm.prescription_type === '饮片'" :span="8">
|
||
<el-form-item label="每贴出包数" prop="bags_per_dose">
|
||
<el-select v-model="editForm.bags_per_dose" placeholder="请选择" class="w-full">
|
||
<el-option label="1包" :value="1" />
|
||
<el-option label="2包" :value="2" />
|
||
<el-option label="3包" :value="3" />
|
||
<el-option label="4包" :value="4" />
|
||
<el-option label="5包" :value="5" />
|
||
<el-option label="6包" :value="6" />
|
||
<el-option label="7包" :value="7" />
|
||
<el-option label="8包" :value="8" />
|
||
<el-option label="9包" :value="9" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="8">
|
||
<el-form-item label="每天几次" prop="times_per_day">
|
||
<el-input-number v-model="editForm.times_per_day" :min="1" :max="6" class="!w-full" placeholder="每天服用次数" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="服用天数" prop="usage_days">
|
||
<el-input-number
|
||
v-model="editForm.usage_days"
|
||
:min="1"
|
||
:max="365"
|
||
placeholder="服用天数"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="用法" prop="usage_instruction">
|
||
<el-input
|
||
v-model="editForm.usage_instruction"
|
||
placeholder="例如:水煎服,一日二次"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="服用时间" prop="usage_time">
|
||
<el-select v-model="editForm.usage_time" placeholder="请选择服用时间" class="w-full">
|
||
<el-option label="饭前" value="饭前" />
|
||
<el-option label="饭后" value="饭后" />
|
||
<el-option label="饭中" value="饭中" />
|
||
<el-option label="空腹" value="空腹" />
|
||
<el-option label="睡前" value="睡前" />
|
||
<el-option label="晨起" value="晨起" />
|
||
<el-option label="随时" value="随时" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服用方式" prop="usage_way">
|
||
<el-select v-model="editForm.usage_way" placeholder="请选择服用方式" class="w-full">
|
||
<el-option label="温水送服" value="温水送服" />
|
||
<el-option label="开水冲服" value="开水冲服" />
|
||
<el-option label="黄酒送服" value="黄酒送服" />
|
||
<el-option label="淡盐水送服" value="淡盐水送服" />
|
||
<el-option label="米汤送服" value="米汤送服" />
|
||
<el-option label="嚼服" value="嚼服" />
|
||
<el-option label="含化" value="含化" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="忌口" prop="dietary_taboo">
|
||
<el-select
|
||
v-model="editForm.dietary_taboo"
|
||
multiple
|
||
placeholder="请选择忌口内容(可多选)"
|
||
class="w-full"
|
||
>
|
||
<el-option label="辛辣食物" value="辛辣食物" />
|
||
<el-option label="生冷食物" value="生冷食物" />
|
||
<el-option label="油腻食物" value="油腻食物" />
|
||
<el-option label="海鲜" value="海鲜" />
|
||
<el-option label="牛羊肉" value="牛羊肉" />
|
||
<el-option label="鸡蛋" value="鸡蛋" />
|
||
<el-option label="豆制品" value="豆制品" />
|
||
<el-option label="酒类" value="酒类" />
|
||
<el-option label="浓茶" value="浓茶" />
|
||
<el-option label="咖啡" value="咖啡" />
|
||
<el-option label="烟草" value="烟草" />
|
||
<el-option label="萝卜" value="萝卜" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="其他说明" prop="usage_notes">
|
||
<el-input
|
||
v-model="editForm.usage_notes"
|
||
type="textarea"
|
||
:rows="2"
|
||
placeholder="其他服用注意事项"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="医师姓名" prop="doctor_name">
|
||
<el-input v-model="editForm.doctor_name" placeholder="请输入医师姓名" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="医师签名" prop="doctor_signature" required>
|
||
<div class="signature-pad-wrap">
|
||
<canvas
|
||
ref="signatureCanvasRef"
|
||
class="signature-canvas"
|
||
@pointerdown.prevent="onSignaturePointerDown"
|
||
@pointermove.prevent="onSignaturePointerMove"
|
||
@pointerup.prevent="onSignaturePointerUp"
|
||
@pointercancel.prevent="onSignaturePointerUp"
|
||
/>
|
||
<div class="signature-actions">
|
||
<el-button size="small" @click="clearSignature">清空签名</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="text-xs text-amber-600 mt-1">* 必填,请在框内手写医师签名</div>
|
||
</el-form-item>
|
||
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<template v-if="editMode === 'view'">
|
||
<el-button @click="showEdit = false">关闭</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button @click="showEdit = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<!-- 从消费者处方创建业务订单(zyt_tcm_prescription_order,与支付单 zyt_order 分离) -->
|
||
<el-dialog
|
||
v-model="createOrderVisible"
|
||
title="创建业务订单"
|
||
width="940px"
|
||
top="4vh"
|
||
:close-on-click-modal="false"
|
||
@closed="resetCreateOrderForm"
|
||
class="create-order-dialog"
|
||
>
|
||
<div class="create-order-dialog__inner">
|
||
<!-- 关联处方:卡片式信息区,层次更清晰 -->
|
||
<div v-if="createOrderPrescription" class="create-order-rx-card">
|
||
<div class="create-order-rx-card__head">
|
||
<span class="create-order-rx-card__badge">关联处方</span>
|
||
<div class="create-order-rx-card__meta">
|
||
<span class="create-order-rx-card__id">#{{ createOrderPrescription.id }}</span>
|
||
<span v-if="createOrderPrescription.sn" class="create-order-rx-card__sn">{{ createOrderPrescription.sn }}</span>
|
||
<span class="create-order-rx-card__patient">{{ createOrderPrescription.patient_name || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- <el-tooltip :content="createOrderHerbSummary" placement="bottom" :show-after="300">
|
||
<div class="create-order-rx-card__herbs">{{ createOrderHerbSummary }}</div>
|
||
</el-tooltip> -->
|
||
</div>
|
||
|
||
<el-steps
|
||
:active="createOrderStep"
|
||
finish-status="success"
|
||
align-center
|
||
class="create-order-steps"
|
||
>
|
||
<el-step title="患者与收货" description="诊单与物流地址" />
|
||
<el-step title="服务与支付单" description="套餐与关联收款" />
|
||
<el-step title="金额与确认" description="费用与备注" />
|
||
</el-steps>
|
||
|
||
<p class="create-order-step-lead">{{ createOrderStepLead }}</p>
|
||
|
||
<el-form
|
||
ref="createOrderFormRef"
|
||
:model="createOrderForm"
|
||
:rules="createOrderRules"
|
||
label-width="108px"
|
||
class="create-order-form"
|
||
>
|
||
<div v-show="createOrderStep === 0" class="create-order-step-panel">
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="诊单患者" prop="patient_id">
|
||
<el-select
|
||
v-model="createOrderForm.patient_id"
|
||
class="w-full"
|
||
placeholder="搜索姓名/手机/身份证"
|
||
filterable
|
||
remote
|
||
:remote-method="searchPatientsForOrder"
|
||
:loading="createOrderPatientLoading"
|
||
@change="onCreateOrderPatientChange"
|
||
>
|
||
<el-option
|
||
v-for="item in createOrderPatientList"
|
||
:key="item.id"
|
||
:label="`${item.patient_name} (${item.phone || '无手机'})`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="收货人" prop="recipient_name">
|
||
<el-input v-model="createOrderForm.recipient_name" placeholder="姓名" maxlength="50" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="收货手机" prop="recipient_phone">
|
||
<el-input v-model="createOrderForm.recipient_phone" placeholder="物流联系" maxlength="20" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="省市区" prop="region">
|
||
<el-cascader
|
||
v-model="createOrderForm.region"
|
||
:options="regionOptions"
|
||
:props="{ value: 'name', label: 'name', children: 'children', emitPath: true, checkStrictly: false }"
|
||
placeholder="省 / 市 / 区"
|
||
clearable
|
||
filterable
|
||
class="w-full"
|
||
@change="handleRegionChange"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="详细地址" prop="shipping_address">
|
||
<el-input
|
||
v-model="createOrderForm.shipping_address"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="街道、小区、门牌号等详细地址"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div v-show="createOrderStep === 1" class="create-order-step-panel">
|
||
<el-row :gutter="20">
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="复诊">
|
||
<el-switch v-model="createOrderForm.is_follow_up" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="服用天数" class="create-order-form-item--inline-num">
|
||
<div class="flex items-center gap-1 w-full">
|
||
<el-input-number
|
||
v-model="createOrderForm.medication_days"
|
||
:min="1"
|
||
:max="999"
|
||
:step="1"
|
||
controls-position="right"
|
||
class="flex-1 min-w-0"
|
||
/>
|
||
<span class="create-order-unit-suffix">天</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="8">
|
||
<el-form-item label="上次医生">
|
||
<el-input v-model="createOrderForm.prev_staff" placeholder="可选" maxlength="100" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服务渠道">
|
||
<el-input v-model="createOrderForm.service_channel" placeholder="可选" maxlength="100" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服务套餐">
|
||
<el-select
|
||
v-model="createOrderForm.service_package"
|
||
multiple
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
placeholder="请选择服务套餐"
|
||
clearable
|
||
class="w-full"
|
||
>
|
||
<el-option
|
||
v-for="item in servicePackageOptions"
|
||
:key="item.value"
|
||
:label="item.name"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="关联支付单" prop="pay_order_ids">
|
||
<el-select
|
||
v-model="createOrderForm.pay_order_ids"
|
||
class="w-full"
|
||
multiple
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
:placeholder="
|
||
createOrderDepositMin > 0
|
||
? '请至少选择一笔本诊单支付单(已开启门槛)'
|
||
: '多选本诊单支付单(可选)'
|
||
"
|
||
:loading="createOrderPaidOrdersLoading"
|
||
>
|
||
<el-option
|
||
v-for="o in createOrderPaidOrders"
|
||
:key="o.id"
|
||
:label="`${o.order_no} · ¥${o.amount} · ${formatPoCategory(o.order_type)}`"
|
||
:value="o.id"
|
||
class="!h-auto py-1.5"
|
||
>
|
||
<div class="flex flex-col gap-0.5" style="line-height: normal;">
|
||
<div class="flex items-center justify-between">
|
||
<span class="font-medium text-[13px] text-gray-800">{{ o.order_no }}</span>
|
||
<span class="text-orange-500 font-medium ml-4">¥{{ o.amount }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between text-xs text-gray-400 mt-1">
|
||
<span class="truncate pr-2">{{ formatPoCategory(o.order_type) }}<template v-if="o.remark"> · {{ o.remark }}</template></span>
|
||
<span class="shrink-0">{{ String(o.create_time || '').substring(0, 16) }}</span>
|
||
</div>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<div class="create-order-pay-hint text-gray-500">
|
||
<template v-if="createOrderDepositMin > 0">
|
||
<span class="text-amber-600 font-medium">当前已开启关联支付门槛(¥{{ formatPoDepositMoney(createOrderDepositMin) }}),须至少选择一笔支付单。</span>
|
||
<span class="mx-1"> </span>
|
||
</template>
|
||
主管/本诊单医助可见全部支付单;否则仅本人收款单。已被占用的支付单不列出。
|
||
<el-tooltip
|
||
content="主管或本诊单医助可见该诊单全部支付单(待支付/已支付),否则仅本人创建的收款单。已被其他有效业务订单占用的支付单不会出现在此列表。"
|
||
placement="top"
|
||
>
|
||
<span class="text-primary cursor-help ml-0.5">详情</span>
|
||
</el-tooltip>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div v-show="createOrderStep === 2" class="create-order-step-panel">
|
||
<!-- 横向概览,一屏内更省高度 -->
|
||
<div
|
||
class="create-order-deposit-bar"
|
||
:class="createOrderLinkedPaidTotal >= createOrderDepositMin ? 'is-ok' : 'is-warn'"
|
||
>
|
||
<div class="create-order-deposit-bar__cell">
|
||
<span class="create-order-deposit-bar__label">定金门槛</span>
|
||
<strong>¥{{ formatPoDepositMoney(createOrderDepositMin) }}</strong>
|
||
<span class="create-order-deposit-bar__sub">{{ createOrderDepositMin > 0 ? '须 ≥ 此值' : '未设置' }}</span>
|
||
</div>
|
||
<div class="create-order-deposit-bar__sep" />
|
||
<div class="create-order-deposit-bar__cell">
|
||
<span class="create-order-deposit-bar__label">已选收款</span>
|
||
<strong>¥{{ formatPoDepositMoney(createOrderLinkedPaidTotal) }}</strong>
|
||
<span class="create-order-deposit-bar__sub">
|
||
{{ createOrderLinkedPaidTotal >= createOrderDepositMin ? '已达标' : '未达标' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<el-row :gutter="20" class="create-order-step-panel__row-after-bar">
|
||
<el-col :span="12">
|
||
<el-form-item label="费用类别" prop="fee_type">
|
||
<el-select v-model="createOrderForm.fee_type" placeholder="请选择" class="w-full">
|
||
<el-option label="挂号费" :value="1" />
|
||
<el-option label="问诊费" :value="2" />
|
||
<el-option label="药品费用" :value="3" />
|
||
<el-option label="首付费用" :value="4" />
|
||
<el-option label="尾款费用" :value="5" />
|
||
<el-option label="其他费用" :value="6" />
|
||
<el-option label="全部费用" :value="7" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="总金额" prop="amount">
|
||
<el-input-number
|
||
v-model="createOrderForm.amount"
|
||
:min="createOrderAmountInputMin"
|
||
:step="0.01"
|
||
:precision="2"
|
||
class="w-full"
|
||
controls-position="right"
|
||
placeholder="元"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col v-perms="['finance.account_log/lists']" :span="12">
|
||
<el-form-item label="内部成本">
|
||
<el-input-number
|
||
v-model="createOrderForm.internal_cost"
|
||
:min="0"
|
||
:step="0.01"
|
||
:precision="2"
|
||
class="w-full"
|
||
controls-position="right"
|
||
placeholder="财务可见"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="createOrderForm.remark_extra"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="选填,对内说明"
|
||
maxlength="200"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<div class="create-order-footer">
|
||
<div class="create-order-footer__hint">
|
||
<span class="create-order-footer__step">第 {{ createOrderStep + 1 }} / 3 步</span>
|
||
<span v-if="createOrderStep === 2 && createOrderDepositMin > 0" class="create-order-footer__warn">
|
||
订单金额与关联支付单合计须 ≥ ¥{{ formatPoDepositMoney(createOrderDepositMin) }}
|
||
</span>
|
||
</div>
|
||
<div class="create-order-footer__actions">
|
||
<el-button size="large" @click="createOrderVisible = false">取消</el-button>
|
||
<el-button v-if="createOrderStep > 0" size="large" @click="goCreateOrderPrevStep">上一步</el-button>
|
||
<el-button
|
||
v-if="createOrderStep < 2"
|
||
type="primary"
|
||
size="large"
|
||
@click="goCreateOrderNextStep"
|
||
>
|
||
下一步
|
||
</el-button>
|
||
<el-button
|
||
v-if="createOrderStep === 2"
|
||
type="primary"
|
||
size="large"
|
||
:loading="createOrderSubmitLoading"
|
||
@click="submitCreateOrderFromPrescription"
|
||
>
|
||
创建业务订单
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 从处方库导入(与诊间 tcm-prescription 一致,按当前处方开方医师 creator_id 筛选) -->
|
||
<el-dialog
|
||
v-model="showLibraryDialog"
|
||
title="从处方库导入"
|
||
width="800px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<div class="mb-4">
|
||
<el-input
|
||
v-model="librarySearchName"
|
||
placeholder="请输入处方名称搜索"
|
||
clearable
|
||
@keyup.enter="searchLibrary"
|
||
@clear="searchLibrary"
|
||
>
|
||
<template #append>
|
||
<el-button :icon="Search" @click="searchLibrary" />
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="libraryLoading"
|
||
:data="libraryList"
|
||
height="400"
|
||
style="cursor: pointer"
|
||
@row-click="handleSelectLibraryRow"
|
||
>
|
||
<el-table-column label="处方名称" prop="prescription_name" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="药材数量" width="100">
|
||
<template #default="{ row }">
|
||
{{ row.herbs?.length || 0 }}味
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="药材明细" min-width="300" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.herbs && row.herbs.length > 0">
|
||
{{ row.herbs.map((h: any) => `${h.name} ${h.dosage}g`).join('、') }}
|
||
</span>
|
||
<span v-else class="text-gray-400">暂无药材</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建人" prop="creator_name" width="100" />
|
||
<el-table-column label="操作" width="100" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click.stop="handleImportLibrary(row)">导入</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="flex justify-end mt-4">
|
||
<el-pagination
|
||
v-model:current-page="libraryPage"
|
||
v-model:page-size="libraryPageSize"
|
||
:total="libraryTotal"
|
||
:page-sizes="[10, 15, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@current-change="loadLibraryList"
|
||
@size-change="loadLibraryList"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="auditDialogVisible" title="处方审核" width="480px" destroy-on-close @closed="auditRemark = ''">
|
||
<p class="text-sm text-gray-600 mb-3">
|
||
通过:处方保持有效。驳回:将同时作废此处方(与诊间「驳回/作废处方」一致)。
|
||
</p>
|
||
<el-form label-width="80px">
|
||
<el-form-item label="审核意见">
|
||
<el-input
|
||
v-model="auditRemark"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="通过可简要说明;驳回必填"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="auditDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="auditLoading" @click="submitAudit('approve')">审核通过</el-button>
|
||
<el-button type="danger" :loading="auditLoading" @click="submitAudit('reject')">驳回处方</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 与 tcm/diagnosis/edit 同一套诊单界面(只读 + 全部分页签) -->
|
||
<TcmDiagnosisEditView ref="diagnosisViewRef" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup name="prescriptionList">
|
||
import {
|
||
prescriptionLists,
|
||
prescriptionAdd,
|
||
prescriptionEdit,
|
||
prescriptionDelete,
|
||
prescriptionAudit,
|
||
prescriptionDetail,
|
||
prescriptionOrderCreate,
|
||
prescriptionOrderPaidPayOrders,
|
||
getDoctors,
|
||
prescriptionLibraryLists
|
||
} from '@/api/tcm'
|
||
import { searchPatients as searchPatientsAPI } from '@/api/order'
|
||
import { getDictData } from '@/api/app'
|
||
import { roleAll } from '@/api/perms/role'
|
||
import { usePaging } from '@/hooks/usePaging'
|
||
import useUserStore from '@/stores/modules/user'
|
||
import feedback from '@/utils/feedback'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||
import MedicineNameSelect from '@/components/medicine-name-select/index.vue'
|
||
import { Search, Download, Printer } from '@element-plus/icons-vue'
|
||
import { computed, onMounted, reactive, ref, watch, nextTick, defineAsyncComponent } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import html2canvas from 'html2canvas'
|
||
import jsPDF from 'jspdf'
|
||
|
||
const TcmDiagnosisEditView = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
|
||
|
||
/** 与 server/config/project.php prescription_audit_roles 保持一致 */
|
||
const PRESCRIPTION_AUDIT_ROLE_IDS = [0, 3]
|
||
|
||
/** 处方笺抬头/页脚(与诊间打印一致) */
|
||
const SLIP_HOSPITAL_NAME = '成都双流甄养堂互联网医院'
|
||
const SLIP_COMPANY_LINE = '成都双流甄养堂互联网医院有限公司 联系方式:4001667339'
|
||
const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄龙大道二段280号'
|
||
|
||
const userStore = useUserStore()
|
||
const router = useRouter()
|
||
|
||
function goBusinessOrderList() {
|
||
const hit = router.getRoutes().find((r) => r.meta?.perms === 'tcm.prescriptionOrder/lists')
|
||
if (hit?.path) {
|
||
router.push(hit.path)
|
||
} else {
|
||
feedback.msgWarning('未配置「处方业务订单」菜单,请在权限管理中执行菜单 SQL 或手动添加路由')
|
||
}
|
||
}
|
||
|
||
// 表单数据(与列表接口查询参数一致)
|
||
const formData = reactive({
|
||
/** 处方编号(后台 sn,模糊匹配) */
|
||
sn: '',
|
||
patient_name: '',
|
||
/** 处方创建人(医师账号 ID),多选 */
|
||
creator_ids: [] as number[],
|
||
/** all | passed | not_passed | pending | rejected(all 表示不限,接口传参会转为空) */
|
||
audit_filter: 'all' as string,
|
||
/** all | manual | system — 与 is_system_auto:手工(0) / 系统代开(1) */
|
||
source_filter: 'all' as string,
|
||
start_time: '',
|
||
end_time: ''
|
||
})
|
||
|
||
const doctorOptions = ref<Array<{ id: number; name: string; account?: string }>>([])
|
||
|
||
/** 创建时间快捷:all | today | yesterday | before_yesterday | custom | '' */
|
||
const dateQuickTab = ref<string>('all')
|
||
|
||
const showEdit = ref(false)
|
||
const editMode = ref<'add' | 'edit' | 'view'>('add')
|
||
const submitLoading = ref(false)
|
||
const formRef = ref<FormInstance>()
|
||
const signatureCanvasRef = ref<HTMLCanvasElement>()
|
||
/** 当前一笔的 pointerId(配合 setPointerCapture,移出画布仍跟手) */
|
||
const signatureActivePointerId = ref<number | null>(null)
|
||
/** 签名板 CSS 像素尺寸(内部分辨率按 DPR 放大,触摸更跟手) */
|
||
const SIGNATURE_CSS_W = 560
|
||
const SIGNATURE_CSS_H = 168
|
||
/** 查看模式:处方笺数据源(先列表行,再详情覆盖) */
|
||
const slipView = ref<Record<string, any> | null>(null)
|
||
const viewDetailLoading = ref(false)
|
||
|
||
const roleOptions = ref<Array<{ id: number; name: string }>>([])
|
||
|
||
const auditDialogVisible = ref(false)
|
||
const auditTargetId = ref(0)
|
||
const auditRemark = ref('')
|
||
const auditLoading = ref(false)
|
||
|
||
/** 从消费者处方创建订单 */
|
||
const createOrderVisible = ref(false)
|
||
/** 创建订单分步向导:0 患者与收货 / 1 服务与支付单 / 2 金额与确认 */
|
||
const createOrderStep = ref(0)
|
||
|
||
const createOrderStepLead = computed(() => {
|
||
const texts = [
|
||
'请核对诊单患者并填写完整收货信息,便于发货与物流联系。',
|
||
'按需填写服务相关信息,并选择本诊单下需要关联的支付单。',
|
||
'确认费用类别与订单金额;提交前请对照定金门槛是否已满足。'
|
||
]
|
||
return texts[createOrderStep.value] ?? ''
|
||
})
|
||
|
||
/** 第 1 步:患者与收货(分步校验) */
|
||
const CREATE_ORDER_STEP0_FIELDS = ['patient_id', 'recipient_name', 'recipient_phone', 'region', 'shipping_address'] as const
|
||
|
||
const createOrderPrescription = ref<any>(null)
|
||
const createOrderFormRef = ref<FormInstance>()
|
||
const createOrderSubmitLoading = ref(false)
|
||
const createOrderPatientLoading = ref(false)
|
||
const createOrderPatientList = ref<any[]>([])
|
||
const createOrderPaidOrders = ref<Array<{ id: number; order_no?: string; amount?: number | string; order_type?: number | string; remark?: string; create_time?: string }>>([])
|
||
const createOrderPaidOrdersLoading = ref(false)
|
||
const createOrderDepositMin = ref(0)
|
||
|
||
function formatPoDepositMoney(v: unknown) {
|
||
const n = Number(v)
|
||
if (Number.isNaN(n)) return '—'
|
||
return n.toFixed(2)
|
||
}
|
||
|
||
function formatPoCategory(t: unknown) {
|
||
const m: Record<number, string> = {
|
||
1: '挂号费',
|
||
2: '问诊费',
|
||
3: '药品费用',
|
||
4: '首付',
|
||
5: '尾款',
|
||
6: '其他'
|
||
}
|
||
return m[Number(t)] || '未知'
|
||
}
|
||
|
||
const createOrderForm = reactive({
|
||
patient_id: '' as number | string,
|
||
recipient_name: '',
|
||
recipient_phone: '',
|
||
region: [] as string[],
|
||
shipping_address: '',
|
||
is_follow_up: 0,
|
||
medication_days: undefined as number | undefined,
|
||
prev_staff: '',
|
||
service_channel: '',
|
||
service_package: [] as string[],
|
||
express_company: 'auto',
|
||
tracking_number: '',
|
||
fee_type: 3,
|
||
amount: 0,
|
||
internal_cost: undefined as number | undefined,
|
||
remark_extra: '',
|
||
pay_order_ids: [] as number[]
|
||
})
|
||
|
||
// 省市区数据(简化版,实际项目中应该从 API 获取或使用完整的省市区数据)
|
||
const regionOptions = ref([])
|
||
|
||
/** 服务套餐(字典 server_order,与业务订单编辑一致) */
|
||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||
|
||
// 转换省市区数据格式
|
||
const transformRegionData = (data: any[]) => {
|
||
return data.map((province: any) => ({
|
||
name: province.province,
|
||
code: province.code,
|
||
children: province.citys?.map((city: any) => ({
|
||
name: city.city,
|
||
code: city.code,
|
||
children: city.areas?.map((area: any) => ({
|
||
name: area.area,
|
||
code: area.code
|
||
})) || []
|
||
})) || []
|
||
}))
|
||
}
|
||
|
||
// 加载省市区数据
|
||
const loadRegionData = async () => {
|
||
try {
|
||
console.log('开始加载省市区数据...')
|
||
// 开发环境使用代理,生产环境使用环境变量中的基础URL
|
||
const apiUrl = import.meta.env.DEV
|
||
? '/api-proxy/area/ChinaCitys.json'
|
||
: `https://cos.zhenyangtang.com.cn/area/ChinaCitys.json`
|
||
|
||
const response = await fetch(apiUrl)
|
||
console.log('响应状态:', response.status)
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
console.log('加载的数据条数:', data?.length)
|
||
|
||
// 转换数据格式
|
||
regionOptions.value = transformRegionData(data)
|
||
console.log('regionOptions 已设置,条数:', regionOptions.value.length)
|
||
} catch (error) {
|
||
console.error('加载省市区数据失败:', error)
|
||
// 如果加载失败,使用基本的省市数据作为后备
|
||
regionOptions.value = [
|
||
{
|
||
name: '四川省',
|
||
children: [
|
||
{
|
||
name: '成都市',
|
||
children: [
|
||
{ name: '锦江区' },
|
||
{ name: '青羊区' },
|
||
{ name: '金牛区' },
|
||
{ name: '武侯区' },
|
||
{ name: '成华区' },
|
||
{ name: '龙泉驿区' },
|
||
{ name: '青白江区' },
|
||
{ name: '新都区' },
|
||
{ name: '温江区' },
|
||
{ name: '双流区' }
|
||
]
|
||
},
|
||
{
|
||
name: '绵阳市',
|
||
children: [
|
||
{ name: '涪城区' },
|
||
{ name: '游仙区' },
|
||
{ name: '安州区' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
name: '北京市',
|
||
children: [
|
||
{
|
||
name: '北京市',
|
||
children: [
|
||
{ name: '东城区' },
|
||
{ name: '西城区' },
|
||
{ name: '朝阳区' },
|
||
{ name: '丰台区' },
|
||
{ name: '海淀区' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
name: '上海市',
|
||
children: [
|
||
{
|
||
name: '上海市',
|
||
children: [
|
||
{ name: '黄浦区' },
|
||
{ name: '徐汇区' },
|
||
{ name: '长宁区' },
|
||
{ name: '静安区' },
|
||
{ name: '普陀区' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
name: '广东省',
|
||
children: [
|
||
{
|
||
name: '广州市',
|
||
children: [
|
||
{ name: '荔湾区' },
|
||
{ name: '越秀区' },
|
||
{ name: '海珠区' },
|
||
{ name: '天河区' },
|
||
{ name: '白云区' }
|
||
]
|
||
},
|
||
{
|
||
name: '深圳市',
|
||
children: [
|
||
{ name: '罗湖区' },
|
||
{ name: '福田区' },
|
||
{ name: '南山区' },
|
||
{ name: '宝安区' }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
console.log('使用后备数据,条数:', regionOptions.value.length)
|
||
}
|
||
}
|
||
|
||
const loadServicePackageOptions = async () => {
|
||
try {
|
||
const data = await getDictData({ type: 'server_order' })
|
||
servicePackageOptions.value = (data?.server_order || []).filter((item: { status?: number }) => item.status !== 0)
|
||
} catch {
|
||
servicePackageOptions.value = []
|
||
}
|
||
}
|
||
|
||
// 处理省市区选择变化
|
||
const handleRegionChange = (value: string[]) => {
|
||
console.log('选择的省市区:', value)
|
||
// value is an array like ['四川省', '成都市', '锦江区']
|
||
// We'll extract these in the submit function
|
||
}
|
||
|
||
const createOrderLinkedPaidTotal = computed(() => {
|
||
const ids = new Set(createOrderForm.pay_order_ids.map((x) => Number(x)))
|
||
let s = 0
|
||
for (const o of createOrderPaidOrders.value) {
|
||
if (ids.has(Number(o.id))) {
|
||
s += Number(o.amount) || 0
|
||
}
|
||
}
|
||
return Math.round(s * 100) / 100
|
||
})
|
||
|
||
const createOrderAmountInputMin = computed(() => {
|
||
const m = createOrderDepositMin.value
|
||
if (m > 0) {
|
||
return Math.round(m * 100) / 100
|
||
}
|
||
return 0
|
||
})
|
||
|
||
const createOrderRules = computed<FormRules>(() => {
|
||
const rules: FormRules = {
|
||
patient_id: [{ required: true, message: '请选择诊单患者', trigger: 'change' }],
|
||
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
||
recipient_phone: [{ required: true, message: '请输入收货手机', trigger: 'blur' }],
|
||
region: [{ required: true, message: '请选择省/市/区', trigger: 'change', type: 'array' }],
|
||
shipping_address: [{ required: true, message: '请输入收货地址', trigger: 'blur' }],
|
||
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
||
amount: [
|
||
{ required: true, message: '请输入订单金额', trigger: 'blur' },
|
||
{
|
||
validator: (_rule, v, cb) => {
|
||
const min = createOrderDepositMin.value
|
||
const num = Number(v)
|
||
if (min > 0 && (!Number.isFinite(num) || num < min)) {
|
||
cb(new Error(`订单金额须大于等于定金门槛 ¥${formatPoDepositMoney(min)}`))
|
||
return
|
||
}
|
||
cb()
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}
|
||
// 与 .env PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT 一致(接口 deposit_min_amount)
|
||
// 勿在 el-form-item 上设 required,否则会多出一条默认英文「pay_order_ids is required」
|
||
if (createOrderDepositMin.value > 0) {
|
||
const depMsg = `已开启关联支付门槛(¥${formatPoDepositMoney(createOrderDepositMin.value)}),请至少选择一笔支付单`
|
||
rules.pay_order_ids = [
|
||
{
|
||
type: 'array',
|
||
required: true,
|
||
min: 1,
|
||
message: depMsg,
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
return rules
|
||
})
|
||
|
||
const createOrderHerbSummary = computed(() => {
|
||
const row = createOrderPrescription.value
|
||
const herbs = normalizeSlipHerbs(row?.herbs)
|
||
if (!herbs.length) return '暂无药材明细'
|
||
const s = herbs.map((h) => `${h.name} ${h.dosage}g`).join('、')
|
||
return s.length > 120 ? `${s.slice(0, 120)}…` : s
|
||
})
|
||
|
||
function resetCreateOrderForm() {
|
||
createOrderStep.value = 0
|
||
createOrderForm.patient_id = ''
|
||
createOrderForm.recipient_name = ''
|
||
createOrderForm.recipient_phone = ''
|
||
createOrderForm.shipping_address = ''
|
||
createOrderForm.is_follow_up = 0
|
||
createOrderForm.medication_days = undefined
|
||
createOrderForm.prev_staff = ''
|
||
createOrderForm.service_channel = ''
|
||
createOrderForm.service_package = []
|
||
createOrderForm.express_company = 'auto'
|
||
createOrderForm.tracking_number = ''
|
||
createOrderForm.fee_type = 3
|
||
createOrderForm.amount = 0
|
||
createOrderForm.internal_cost = undefined
|
||
createOrderForm.remark_extra = ''
|
||
createOrderForm.pay_order_ids = []
|
||
createOrderPatientList.value = []
|
||
createOrderPaidOrders.value = []
|
||
createOrderDepositMin.value = 0
|
||
createOrderPrescription.value = null
|
||
createOrderFormRef.value?.clearValidate()
|
||
}
|
||
|
||
/** 仅按定金门槛抬升「总金额」,不跟已选支付单合计联动(由用户自填) */
|
||
function syncCreateOrderAmountFromRules() {
|
||
const min = createOrderDepositMin.value
|
||
if (min <= 0) return
|
||
const need = Math.round(min * 100) / 100
|
||
if (Number(createOrderForm.amount) < need) {
|
||
createOrderForm.amount = need
|
||
}
|
||
}
|
||
|
||
async function loadCreateOrderPaidOrders() {
|
||
const did = Number(createOrderForm.patient_id)
|
||
if (!did) {
|
||
createOrderPaidOrders.value = []
|
||
createOrderDepositMin.value = 0
|
||
return
|
||
}
|
||
try {
|
||
createOrderPaidOrdersLoading.value = true
|
||
const res: any = await prescriptionOrderPaidPayOrders({
|
||
diagnosis_id: did
|
||
})
|
||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||
createOrderPaidOrders.value = Array.isArray(lists) ? lists : []
|
||
const dep = Number(res?.deposit_min_amount ?? res?.data?.deposit_min_amount) || 0
|
||
createOrderDepositMin.value = dep
|
||
syncCreateOrderAmountFromRules()
|
||
} catch {
|
||
createOrderPaidOrders.value = []
|
||
createOrderDepositMin.value = 0
|
||
} finally {
|
||
createOrderPaidOrdersLoading.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => createOrderForm.patient_id,
|
||
() => {
|
||
createOrderForm.pay_order_ids = []
|
||
void loadCreateOrderPaidOrders()
|
||
}
|
||
)
|
||
|
||
async function openCreateOrderFromPrescription(row: any) {
|
||
if (Number(row?.void_status) === 1) {
|
||
feedback.msgWarning('已作废处方不可创建业务订单')
|
||
return
|
||
}
|
||
resetCreateOrderForm()
|
||
createOrderPrescription.value = row
|
||
const did = Number(row.diagnosis_id) || 0
|
||
if (did > 0) {
|
||
createOrderForm.patient_id = did
|
||
createOrderForm.recipient_name = row.patient_name || ''
|
||
createOrderForm.recipient_phone = String(row.phone || '').trim()
|
||
createOrderPatientList.value = [
|
||
{
|
||
id: did,
|
||
patient_name: row.patient_name || '',
|
||
phone: String(row.phone || '').trim()
|
||
}
|
||
]
|
||
}
|
||
const ud = Number(row.usage_days)
|
||
if (ud > 0) {
|
||
createOrderForm.medication_days = ud
|
||
}
|
||
createOrderVisible.value = true
|
||
void loadCreateOrderPaidOrders()
|
||
const rxId = row.id
|
||
if (rxId && !createOrderForm.recipient_phone) {
|
||
try {
|
||
const d = (await prescriptionDetail({ id: rxId })) as Record<string, unknown> | null
|
||
const phone = d && typeof d.phone === 'string' ? d.phone.trim() : ''
|
||
if (phone && createOrderPrescription.value?.id === rxId) {
|
||
createOrderForm.recipient_phone = phone
|
||
const first = createOrderPatientList.value[0]
|
||
if (first && Number(first.id) === did) {
|
||
first.phone = phone
|
||
}
|
||
}
|
||
} catch {
|
||
/* 列表无电话时尽力补全,失败忽略 */
|
||
}
|
||
}
|
||
}
|
||
|
||
async function searchPatientsForOrder(query: string) {
|
||
if (!query) {
|
||
createOrderPatientList.value = []
|
||
return
|
||
}
|
||
try {
|
||
createOrderPatientLoading.value = true
|
||
const res: any = await searchPatientsAPI({
|
||
keyword: query,
|
||
page_no: 1,
|
||
page_size: 10
|
||
})
|
||
createOrderPatientList.value = res?.lists || []
|
||
} catch {
|
||
createOrderPatientList.value = []
|
||
} finally {
|
||
createOrderPatientLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onCreateOrderPatientChange(id: number | string) {
|
||
const p = createOrderPatientList.value.find((x) => Number(x.id) === Number(id))
|
||
if (p) {
|
||
createOrderForm.recipient_name = p.patient_name || ''
|
||
createOrderForm.recipient_phone = p.phone || ''
|
||
}
|
||
}
|
||
|
||
function goCreateOrderPrevStep() {
|
||
if (createOrderStep.value > 0) {
|
||
createOrderStep.value -= 1
|
||
}
|
||
}
|
||
|
||
async function goCreateOrderNextStep() {
|
||
const form = createOrderFormRef.value
|
||
if (!form) return
|
||
try {
|
||
if (createOrderStep.value === 0) {
|
||
await form.validateField([...CREATE_ORDER_STEP0_FIELDS])
|
||
} else if (createOrderStep.value === 1 && createOrderDepositMin.value > 0) {
|
||
await form.validateField(['pay_order_ids'])
|
||
}
|
||
if (createOrderStep.value < 2) {
|
||
createOrderStep.value += 1
|
||
}
|
||
} catch {
|
||
/* 校验未通过,留在当前步 */
|
||
}
|
||
}
|
||
|
||
async function submitCreateOrderFromPrescription() {
|
||
if (!createOrderFormRef.value || !createOrderPrescription.value) return
|
||
if (createOrderStep.value !== 2) return
|
||
try {
|
||
await createOrderFormRef.value.validate()
|
||
createOrderSubmitLoading.value = true
|
||
const diagnosisId = Number(createOrderForm.patient_id)
|
||
const payload: Record<string, unknown> = {
|
||
prescription_id: createOrderPrescription.value.id,
|
||
diagnosis_id: diagnosisId,
|
||
recipient_name: createOrderForm.recipient_name,
|
||
recipient_phone: createOrderForm.recipient_phone,
|
||
shipping_address: createOrderForm.shipping_address,
|
||
is_follow_up: createOrderForm.is_follow_up,
|
||
prev_staff: createOrderForm.prev_staff || '',
|
||
service_channel: createOrderForm.service_channel || '',
|
||
service_package:
|
||
Array.isArray(createOrderForm.service_package) && createOrderForm.service_package.length > 0
|
||
? createOrderForm.service_package.join(',')
|
||
: '',
|
||
express_company: createOrderForm.express_company || 'auto',
|
||
tracking_number: createOrderForm.tracking_number || '',
|
||
fee_type: createOrderForm.fee_type,
|
||
amount: createOrderForm.amount,
|
||
remark_extra: createOrderForm.remark_extra || ''
|
||
}
|
||
|
||
// 添加省市区字段
|
||
if (createOrderForm.region && createOrderForm.region.length >= 3) {
|
||
payload.shipping_province = createOrderForm.region[0]
|
||
payload.shipping_city = createOrderForm.region[1]
|
||
payload.shipping_district = createOrderForm.region[2]
|
||
}
|
||
|
||
if (createOrderForm.medication_days != null && createOrderForm.medication_days > 0) {
|
||
payload.medication_days = createOrderForm.medication_days
|
||
}
|
||
if (createOrderForm.internal_cost != null && createOrderForm.internal_cost > 0) {
|
||
payload.internal_cost = createOrderForm.internal_cost
|
||
}
|
||
if (createOrderForm.pay_order_ids.length) {
|
||
payload.pay_order_ids = [...createOrderForm.pay_order_ids]
|
||
}
|
||
const res: any = await prescriptionOrderCreate(payload)
|
||
const row = res?.data ?? res
|
||
const orderNo = row?.order_no
|
||
feedback.msgSuccess(orderNo ? `业务订单已创建,单号:${orderNo}` : '业务订单已创建')
|
||
createOrderVisible.value = false
|
||
getLists()
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
createOrderSubmitLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 正在编辑的这条是否原为已作废 / 已驳回(用于提示) */
|
||
const editingWasVoid = ref(false)
|
||
const editingWasRejected = ref(false)
|
||
|
||
const editReviveHint = computed(() => {
|
||
const v = editingWasVoid.value
|
||
const r = editingWasRejected.value
|
||
if (!v && !r) return ''
|
||
if (v && r) return '当前处方已作废且已驳回。保存后将取消作废、清除驳回,并重新进入待审核。'
|
||
if (v) return '当前为已作废处方。保存后将取消作废,并重新进入待审核。'
|
||
return '当前为已驳回处方。保存后将清除驳回,并重新进入待审核。'
|
||
})
|
||
|
||
const slipHerbsList = computed(() => {
|
||
const h = slipView.value?.herbs
|
||
return Array.isArray(h) ? h : []
|
||
})
|
||
|
||
const slipDietaryText = computed(() => {
|
||
const d = slipView.value?.dietary_taboo
|
||
if (Array.isArray(d)) return d.filter(Boolean).join('、')
|
||
if (typeof d === 'string' && d.trim()) return d.trim()
|
||
return ''
|
||
})
|
||
|
||
/* ============================================================
|
||
* A4 处方笺(药房联)相关计算
|
||
* ============================================================ */
|
||
const rxPaperRef = ref<HTMLElement | null>(null)
|
||
const downloadingSlip = ref(false)
|
||
|
||
const padN = (n: number) => String(n).padStart(2, '0')
|
||
|
||
const rxDateText = computed(() => {
|
||
const v = slipView.value
|
||
if (!v) return '—'
|
||
const ts = (v as any).create_time ?? (v as any).update_time ?? null
|
||
if (ts) {
|
||
const t = String(ts).length === 10 ? Number(ts) * 1000 : Number(ts)
|
||
const d = new Date(t)
|
||
if (!isNaN(d.getTime())) {
|
||
return `${d.getFullYear()}-${padN(d.getMonth() + 1)}-${padN(d.getDate())} ${padN(d.getHours())}:${padN(d.getMinutes())}`
|
||
}
|
||
}
|
||
return v.prescription_date || '—'
|
||
})
|
||
|
||
const rxSerialText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return '—'
|
||
return (
|
||
v.order_no ||
|
||
v.serial_no ||
|
||
v.serial_number ||
|
||
v.no ||
|
||
v.prescription_no ||
|
||
v.visit_no ||
|
||
(v.id ? `G${v.id}` : '—')
|
||
)
|
||
})
|
||
|
||
const rxRecipientText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return '—'
|
||
if (v.recipient_text) return v.recipient_text
|
||
const parts: string[] = []
|
||
if (v.recipient_name || v.patient_name) parts.push(v.recipient_name || v.patient_name)
|
||
if (v.recipient_phone || v.phone) parts.push(v.recipient_phone || v.phone)
|
||
let region = v.region
|
||
if (Array.isArray(region)) region = region.filter(Boolean).join('')
|
||
if (region) parts.push(String(region))
|
||
if (v.shipping_address) parts.push(String(v.shipping_address))
|
||
else if (v.address) parts.push(String(v.address))
|
||
return parts.length ? parts.join(',') : '—'
|
||
})
|
||
|
||
const rxDoseCount = computed(() => {
|
||
const n = Number(slipView.value?.dose_count)
|
||
return Number.isFinite(n) && n > 0 ? n : 1
|
||
})
|
||
|
||
const rxPerDoseAmount = computed(() => {
|
||
const list = slipHerbsList.value
|
||
const sum = list.reduce((acc: number, h: any) => acc + (Number(h?.dosage) || 0), 0)
|
||
return formatNum(sum)
|
||
})
|
||
|
||
const rxHerbTotal = (dosage: number | string) => {
|
||
const d = Number(dosage) || 0
|
||
return formatNum(d * rxDoseCount.value)
|
||
}
|
||
|
||
const rxTypeText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return '—'
|
||
const t = v.prescription_type || '浓缩水丸'
|
||
return /丸|散|膏|片/.test(t) ? `浓缩丸-${t}` : t
|
||
})
|
||
|
||
const rxUsageText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return '—'
|
||
if (v.usage_text) return v.usage_text
|
||
const times = Number(v.times_per_day) > 0 ? Number(v.times_per_day) : 3
|
||
const bags = Number(v.bags_per_dose) > 0 ? Number(v.bags_per_dose) : 1
|
||
const amount = v.dosage_amount != null ? Number(v.dosage_amount) : 10
|
||
const unit = v.dosage_unit || 'g'
|
||
const usageWay = v.usage_way || '温水送服'
|
||
const usageTime = v.usage_time || ''
|
||
const seg: string[] = []
|
||
seg.push(`每天${times}次`)
|
||
seg.push(`一次${bags}包(约${amount}${unit})`)
|
||
seg.push(`每包${amount}${unit}`)
|
||
seg.push(usageWay)
|
||
if (usageTime) seg.push(usageTime)
|
||
return seg.join(', ')
|
||
})
|
||
|
||
const rxAdviceText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return ''
|
||
/** 出丸克数已经在底部以红色独立行展示,这里不重复 */
|
||
return v.medical_advice || ''
|
||
})
|
||
|
||
const rxRemarkText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return ''
|
||
const parts: string[] = []
|
||
parts.push(`共${slipHerbsList.value.length}味药`)
|
||
if (v.usage_notes) parts.push(String(v.usage_notes))
|
||
if (v.remark) parts.push(String(v.remark))
|
||
return parts.join(' ')
|
||
})
|
||
|
||
const rxPharmacyRemarkText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return ''
|
||
return v.pharmacy_remark || v.pharmacy_note || ''
|
||
})
|
||
|
||
const rxOutPelletText = computed(() => {
|
||
const v = slipView.value as any
|
||
if (!v) return ''
|
||
const out = computeOutPellet(v)
|
||
return out ? `${out}克` : ''
|
||
})
|
||
|
||
function computeOutPellet(v: any): string {
|
||
if (!v) return ''
|
||
const explicit = v.out_pellet || v.total_weight
|
||
if (explicit && Number(explicit) > 0) return formatNum(Number(explicit))
|
||
const list = slipHerbsList.value
|
||
if (!list.length) return ''
|
||
const perDose = list.reduce((acc: number, h: any) => acc + (Number(h?.dosage) || 0), 0)
|
||
return formatNum(perDose * rxDoseCount.value)
|
||
}
|
||
|
||
function formatNum(n: number): string {
|
||
if (!isFinite(n)) return '0'
|
||
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '')
|
||
}
|
||
|
||
const rxFee = computed(() => {
|
||
const v = slipView.value as any
|
||
const medicine = pickFee(v, ['medicine_fee', 'medicine_amount', 'drug_fee', 'pay_amount']) || 0
|
||
const consult = pickFee(v, ['consult_fee', 'consultation_fee']) || 0
|
||
const making = pickFee(v, ['making_fee', 'process_fee', 'manufacturing_fee']) || 0
|
||
const shipping = pickFee(v, ['shipping_fee', 'freight_fee', 'delivery_fee']) || 0
|
||
const discount = pickFee(v, ['discount_amount', 'discount_fee', 'discount']) || 0
|
||
/** 业务订单合计金额(来自后端 detail 注入的 order_amount) */
|
||
const orderAmount = pickFee(v, ['order_amount']) || 0
|
||
let total = pickFee(v, ['total_amount', 'grand_total', 'amount_total']) || 0
|
||
if (!total) total = orderAmount
|
||
if (!total) total = Math.max(0, medicine + consult + making + shipping - discount)
|
||
return {
|
||
medicine: formatYuan(medicine),
|
||
consult: formatYuan(consult),
|
||
making: formatYuan(making),
|
||
shipping: formatYuan(shipping),
|
||
discount: formatYuan(discount),
|
||
total: formatYuan(total)
|
||
}
|
||
})
|
||
|
||
function pickFee(v: any, keys: string[]): number {
|
||
if (!v) return 0
|
||
for (const k of keys) {
|
||
const val = v[k]
|
||
if (val == null || val === '') continue
|
||
const n = Number(val)
|
||
if (!isNaN(n)) return n
|
||
}
|
||
return 0
|
||
}
|
||
|
||
function formatYuan(n: number): string {
|
||
if (!isFinite(n)) return '0'
|
||
return n.toFixed(2).replace(/\.?0+$/, '') || '0'
|
||
}
|
||
|
||
/* === 打印 === */
|
||
function handlePrintSlip() {
|
||
if (!slipView.value) return
|
||
nextTick(() => {
|
||
try {
|
||
window.print()
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
})
|
||
}
|
||
|
||
/* === 下载 PDF(A4) === */
|
||
async function handleDownloadSlip() {
|
||
const node = rxPaperRef.value
|
||
if (!node) {
|
||
feedback.msgWarning('处方笺尚未渲染完成')
|
||
return
|
||
}
|
||
downloadingSlip.value = true
|
||
try {
|
||
await nextTick()
|
||
/** 等一帧让浏览器完成最后的布局/字体回流 */
|
||
await new Promise((r) => requestAnimationFrame(() => r(null)))
|
||
await new Promise((r) => setTimeout(r, 250))
|
||
|
||
/** 走 html2canvas 默认(DOM 克隆 + stylesheet 复制)模式:
|
||
* - 它会把页面 <style>/<link> 表全部复制到 iframe 渲染上下文,
|
||
* Vue scoped 样式与 Tailwind 工具类都能生效;
|
||
* - 不要开 foreignObjectRendering:它不携带外部样式表,会导致空白。
|
||
*/
|
||
const canvas = await html2canvas(node, {
|
||
scale: 2,
|
||
backgroundColor: '#ffffff',
|
||
useCORS: true,
|
||
allowTaint: false,
|
||
logging: false,
|
||
/** 锁定渲染尺寸 = 元素自身尺寸,按 A4 真实宽度排版 */
|
||
windowWidth: node.scrollWidth,
|
||
windowHeight: node.scrollHeight,
|
||
width: node.scrollWidth,
|
||
height: node.scrollHeight
|
||
})
|
||
const imgData = canvas.toDataURL('image/jpeg', 0.95)
|
||
const pdf = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' })
|
||
const pageW = 210
|
||
const pageH = 297
|
||
const imgW = pageW
|
||
const imgH = (canvas.height * imgW) / canvas.width
|
||
|
||
if (imgH <= pageH) {
|
||
pdf.addImage(imgData, 'JPEG', 0, 0, imgW, imgH)
|
||
} else {
|
||
let leftHeight = imgH
|
||
let position = 0
|
||
while (leftHeight > 0) {
|
||
pdf.addImage(imgData, 'JPEG', 0, position, imgW, imgH)
|
||
leftHeight -= pageH
|
||
position -= pageH
|
||
if (leftHeight > 0) pdf.addPage()
|
||
}
|
||
}
|
||
const name = (slipView.value as any)?.patient_name || '处方'
|
||
const id = (slipView.value as any)?.id || ''
|
||
const date = (slipView.value as any)?.prescription_date || ''
|
||
pdf.save(`处方-${name}${date ? '-' + date : ''}${id ? '-' + id : ''}.pdf`)
|
||
} catch (e: any) {
|
||
console.error('下载处方 PDF 失败:', e)
|
||
feedback.msgError(e?.message || '下载失败,请重试')
|
||
} finally {
|
||
downloadingSlip.value = false
|
||
}
|
||
}
|
||
|
||
// 编辑表单
|
||
const editForm = reactive({
|
||
id: 0,
|
||
prescription_type: '浓缩水丸',
|
||
dosage_amount: 1 as number | undefined,
|
||
dosage_unit: 'g',
|
||
need_decoction: false,
|
||
bags_per_dose: 1,
|
||
patient_name: '',
|
||
gender: 1,
|
||
age: 0,
|
||
visit_no: '',
|
||
prescription_date: '',
|
||
tongue: '',
|
||
tongue_image: '',
|
||
pulse: '',
|
||
pulse_condition: '',
|
||
clinical_diagnosis: '',
|
||
herbs: [] as Array<{ name: string; dosage: number }>,
|
||
dose_count: 7,
|
||
dose_unit: '剂',
|
||
usage_days: 7,
|
||
usage_instruction: '',
|
||
usage_time: '饭后',
|
||
usage_way: '温水送服',
|
||
dietary_taboo: [] as string[],
|
||
usage_notes: '',
|
||
doctor_name: '',
|
||
/** 医师电子签名 PNG data URL(与诊间处方一致) */
|
||
doctor_signature: '',
|
||
is_shared: 0,
|
||
visible_role_ids: [] as number[],
|
||
audit_status: 1,
|
||
audit_time: null as number | null,
|
||
audit_by_name: '',
|
||
audit_remark: '',
|
||
diagnosis_id: 0,
|
||
/** 开方医师 admin id(处方库导入按此医生筛选) */
|
||
creator_id: 0,
|
||
/** 1=系统自动生成(如挂号完成) */
|
||
is_system_auto: 0,
|
||
/** 列表带入:业务订单「处方审核」驳回(消费者处方本身可能仍为已通过) */
|
||
business_prescription_audit_rejected: 0,
|
||
business_prescription_audit_remark: '',
|
||
times_per_day: 2
|
||
})
|
||
|
||
const diagnosisViewRef = ref<{
|
||
openViewOnly: (id: number) => Promise<void>
|
||
} | null>(null)
|
||
|
||
function openDiagnosisPatientDetail() {
|
||
const id = Number(editForm.diagnosis_id)
|
||
if (!id) {
|
||
feedback.msgWarning('本处方未关联诊单,无法查看诊单患者信息')
|
||
return
|
||
}
|
||
void diagnosisViewRef.value?.openViewOnly(id)
|
||
}
|
||
|
||
// 标志:是否正在加载数据(防止watch函数覆盖已加载的值)
|
||
const isLoadingData = ref(false)
|
||
|
||
// 监听处方类型变化,自动调整用量单位和默认值
|
||
watch(() => editForm.prescription_type, (newType) => {
|
||
// 如果正在加载数据,不要自动重置值
|
||
if (isLoadingData.value) {
|
||
return
|
||
}
|
||
|
||
if (newType === '浓缩水丸') {
|
||
editForm.dosage_unit = 'g'
|
||
editForm.dosage_amount = 1
|
||
editForm.need_decoction = false
|
||
editForm.bags_per_dose = 1
|
||
} else if (newType === '饮片') {
|
||
editForm.dosage_unit = 'ml'
|
||
editForm.dosage_amount = 50
|
||
editForm.bags_per_dose = 1
|
||
// need_decoction 保持用户选择
|
||
} else {
|
||
editForm.dosage_unit = 'g'
|
||
editForm.dosage_amount = undefined
|
||
editForm.need_decoction = false
|
||
editForm.bags_per_dose = 1
|
||
}
|
||
})
|
||
|
||
// 表单验证规则
|
||
const rules: FormRules = {
|
||
patient_name: [
|
||
{ required: true, message: '请输入患者姓名', trigger: 'blur' }
|
||
],
|
||
gender: [
|
||
{ required: true, message: '请选择性别', trigger: 'change' }
|
||
],
|
||
prescription_date: [
|
||
{ required: true, message: '请选择处方日期', trigger: 'change' }
|
||
],
|
||
clinical_diagnosis: [
|
||
{ required: true, message: '请输入临床诊断', trigger: 'blur' }
|
||
],
|
||
dose_count: [
|
||
{ required: true, message: '请输入剂数', trigger: 'blur' }
|
||
],
|
||
doctor_name: [
|
||
{ required: true, message: '请输入医师姓名', trigger: 'blur' }
|
||
],
|
||
doctor_signature: [
|
||
{
|
||
required: true,
|
||
validator: (_rule, v, cb) => {
|
||
if (!v || !String(v).trim()) {
|
||
cb(new Error('请使用电子签名板手写医师签名'))
|
||
} else {
|
||
cb()
|
||
}
|
||
},
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
async function fetchPrescriptionLists(params: Record<string, unknown>) {
|
||
const { audit_filter, source_filter, creator_ids, ...rest } = params
|
||
const payload: Record<string, unknown> = {
|
||
...rest,
|
||
audit_filter: audit_filter === 'all' || audit_filter === '' ? '' : audit_filter,
|
||
source_filter:
|
||
source_filter === 'all' || source_filter === '' || source_filter === undefined ? '' : source_filter
|
||
}
|
||
if (Array.isArray(creator_ids) && creator_ids.length > 0) {
|
||
payload.creator_ids = creator_ids
|
||
}
|
||
return prescriptionLists(payload)
|
||
}
|
||
|
||
const { pager, getLists, resetPage } = usePaging({
|
||
fetchFun: fetchPrescriptionLists,
|
||
params: formData
|
||
})
|
||
|
||
function pad2(n: number) {
|
||
return String(n).padStart(2, '0')
|
||
}
|
||
|
||
/** 本地日历日的 00:00:00 ~ 23:59:59 字符串(与 daterange-picker 一致) */
|
||
function dayRangeStrings(d: Date) {
|
||
const y = d.getFullYear()
|
||
const m = pad2(d.getMonth() + 1)
|
||
const day = pad2(d.getDate())
|
||
const base = `${y}-${m}-${day}`
|
||
return { start: `${base} 00:00:00`, end: `${base} 23:59:59` }
|
||
}
|
||
|
||
function onDateQuickTabChange(val: string | number | boolean | undefined) {
|
||
const tab = String(val ?? 'all')
|
||
if (tab === 'all') {
|
||
formData.start_time = ''
|
||
formData.end_time = ''
|
||
} else {
|
||
const now = new Date()
|
||
const t = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
if (tab === 'yesterday') {
|
||
t.setDate(t.getDate() - 1)
|
||
} else if (tab === 'before_yesterday') {
|
||
t.setDate(t.getDate() - 2)
|
||
}
|
||
const { start, end } = dayRangeStrings(t)
|
||
formData.start_time = start
|
||
formData.end_time = end
|
||
}
|
||
resetPage()
|
||
}
|
||
|
||
function onAuditFilterChange() {
|
||
resetPage()
|
||
}
|
||
|
||
function onSourceFilterChange() {
|
||
resetPage()
|
||
}
|
||
|
||
let syncingDateTab = false
|
||
watch(
|
||
() => [formData.start_time, formData.end_time],
|
||
() => {
|
||
if (syncingDateTab) return
|
||
const s = formData.start_time
|
||
const e = formData.end_time
|
||
if (!s && !e) {
|
||
dateQuickTab.value = 'all'
|
||
return
|
||
}
|
||
if (!s || !e) {
|
||
dateQuickTab.value = ''
|
||
return
|
||
}
|
||
const now = new Date()
|
||
const matchOffset = (offset: number) => {
|
||
const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - offset)
|
||
const { start, end } = dayRangeStrings(d)
|
||
return s === start && e === end
|
||
}
|
||
if (matchOffset(0)) dateQuickTab.value = 'today'
|
||
else if (matchOffset(1)) dateQuickTab.value = 'yesterday'
|
||
else if (matchOffset(2)) dateQuickTab.value = 'before_yesterday'
|
||
else dateQuickTab.value = ''
|
||
}
|
||
)
|
||
|
||
function formatListCreateTime(v: unknown) {
|
||
if (v === null || v === undefined || v === '') return '—'
|
||
if (typeof v === 'number' && v >= 1e9 && v < 1e11) {
|
||
return formatAuditTime(v)
|
||
}
|
||
const s = String(v).trim()
|
||
if (/^\d{10}$/.test(s)) {
|
||
return formatAuditTime(parseInt(s, 10))
|
||
}
|
||
return s
|
||
}
|
||
|
||
function resetParams() {
|
||
syncingDateTab = true
|
||
dateQuickTab.value = 'all'
|
||
formData.sn = ''
|
||
formData.patient_name = ''
|
||
formData.creator_ids = []
|
||
formData.audit_filter = 'all'
|
||
formData.source_filter = 'all'
|
||
formData.start_time = ''
|
||
formData.end_time = ''
|
||
syncingDateTab = false
|
||
resetPage()
|
||
}
|
||
|
||
function userCanAudit() {
|
||
const u = userStore.userInfo
|
||
if (!u) return false
|
||
if (Number(u.root) === 1) return true
|
||
const raw = u.role_ids ?? u.role_id
|
||
const ids = Array.isArray(raw) ? raw.map((n: unknown) => Number(n)) : []
|
||
return PRESCRIPTION_AUDIT_ROLE_IDS.some((rid) => ids.includes(rid))
|
||
}
|
||
|
||
function auditStatusLabel(v: number | undefined) {
|
||
if (v === 0) return '待审核'
|
||
if (v === 2) return '已驳回'
|
||
return '已通过'
|
||
}
|
||
|
||
function auditStatusTagType(v: number | undefined) {
|
||
if (v === 0) return 'warning'
|
||
if (v === 2) return 'danger'
|
||
return 'success'
|
||
}
|
||
|
||
/** 业务订单处方审核驳回时,列表「审核状态」主标签显示为已驳回 */
|
||
function listAuditStatusLabel(row: { audit_status?: number; business_prescription_audit_rejected?: number }) {
|
||
if (Number(row.business_prescription_audit_rejected) === 1) return '已驳回'
|
||
return auditStatusLabel(row.audit_status)
|
||
}
|
||
|
||
function listAuditStatusTagType(row: { audit_status?: number; business_prescription_audit_rejected?: number }) {
|
||
if (Number(row.business_prescription_audit_rejected) === 1) return 'danger'
|
||
return auditStatusTagType(row.audit_status)
|
||
}
|
||
|
||
function listRejectReasonLines(row: {
|
||
audit_status?: number
|
||
audit_remark?: string
|
||
business_prescription_audit_rejected?: number
|
||
business_prescription_audit_remark?: string
|
||
}): string[] {
|
||
const out: string[] = []
|
||
if (Number(row.business_prescription_audit_rejected) === 1) {
|
||
const t = String(row.business_prescription_audit_remark || '').trim()
|
||
out.push(t ? `业务订单审核驳回意见:${t}` : '业务订单审核驳回意见:—')
|
||
}
|
||
if (Number(row.audit_status) === 2) {
|
||
const t = String(row.audit_remark || '').trim()
|
||
out.push(t ? `消费者处方审核驳回意见:${t}` : '消费者处方审核驳回意见:—')
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** 已通过审核且未作废:仅可查看,不可编辑/删除(与预约列表「查看」一致) */
|
||
function isApprovedActivePrescription(row: { audit_status?: number; void_status?: number }) {
|
||
return Number(row.audit_status) === 1 && Number(row.void_status) !== 1
|
||
}
|
||
|
||
function formatAuditTime(ts: number | null | undefined) {
|
||
if (!ts) return ''
|
||
const d = new Date(ts * 1000)
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||
}
|
||
|
||
function formatVoidTime(ts: number | null | undefined) {
|
||
if (!ts) return '—'
|
||
return formatAuditTime(ts)
|
||
}
|
||
|
||
function slipGenderLabel(g: unknown) {
|
||
if (g === 1 || g === '1') return '男'
|
||
if (g === 0 || g === '0') return '女'
|
||
return '—'
|
||
}
|
||
|
||
function slipAgeText(age: unknown) {
|
||
if (age === null || age === undefined || age === '') return '—'
|
||
return `${age}岁`
|
||
}
|
||
|
||
function normalizeSlipHerbs(raw: unknown): Array<{ name: string; dosage: number }> {
|
||
if (!raw) return []
|
||
if (Array.isArray(raw)) {
|
||
return raw.map((x: any) => ({
|
||
name: String(x?.name ?? '').trim(),
|
||
dosage: Number(x?.dosage) || 0
|
||
}))
|
||
}
|
||
return []
|
||
}
|
||
|
||
function listRxOrderWarnings(row: any): string[] {
|
||
if (Number(row?.has_prescription_order) !== 1) return []
|
||
const herbs = normalizeSlipHerbs(row?.herbs)
|
||
const herbNames = herbs
|
||
.map((h) => String(h?.name || '').trim())
|
||
.filter(Boolean)
|
||
const warns: string[] = []
|
||
|
||
if (!herbNames.length) {
|
||
warns.push('请开方,当前处方药材为空白')
|
||
return warns
|
||
}
|
||
|
||
const seen = new Set<string>()
|
||
const dup = new Set<string>()
|
||
for (const n of herbNames) {
|
||
const k = n.replace(/\s+/g, '').toLowerCase()
|
||
if (!k) continue
|
||
if (seen.has(k)) dup.add(n)
|
||
else seen.add(k)
|
||
}
|
||
if (dup.size > 0) {
|
||
warns.push(`已有关联业务订单,当前处方存在重复药材:${Array.from(dup).join('、')}`)
|
||
}
|
||
return warns
|
||
}
|
||
|
||
function rowToSlipView(row: any) {
|
||
return {
|
||
...row,
|
||
herbs: normalizeSlipHerbs(row?.herbs)
|
||
}
|
||
}
|
||
|
||
function onEditDialogClosed() {
|
||
slipView.value = null
|
||
signatureActivePointerId.value = null
|
||
}
|
||
|
||
/**
|
||
* 签名坐标:与 initSignatureCanvas 里 ctx.scale(dpr) 后的「逻辑像素」一致(0…SIGNATURE_CSS_W/H),
|
||
* 不能用 canvas.width/rect.width(会把 dpr 乘两次,笔画和鼠标严重错位)。
|
||
*/
|
||
function getSignatureLocalPoint(e: PointerEvent) {
|
||
const canvas = signatureCanvasRef.value
|
||
if (!canvas) return { x: 0, y: 0 }
|
||
const rect = canvas.getBoundingClientRect()
|
||
if (rect.width <= 0 || rect.height <= 0) return { x: 0, y: 0 }
|
||
const x = ((e.clientX - rect.left) / rect.width) * SIGNATURE_CSS_W
|
||
const y = ((e.clientY - rect.top) / rect.height) * SIGNATURE_CSS_H
|
||
return {
|
||
x: Math.max(0, Math.min(SIGNATURE_CSS_W, x)),
|
||
y: Math.max(0, Math.min(SIGNATURE_CSS_H, y))
|
||
}
|
||
}
|
||
|
||
function commitSignatureImage() {
|
||
const canvas = signatureCanvasRef.value
|
||
if (canvas) {
|
||
editForm.doctor_signature = canvas.toDataURL('image/png')
|
||
}
|
||
nextTick(() => {
|
||
formRef.value?.validateField('doctor_signature').catch(() => {})
|
||
})
|
||
}
|
||
|
||
function onSignaturePointerDown(e: PointerEvent) {
|
||
if (e.pointerType === 'mouse' && e.button !== 0) return
|
||
const canvas = signatureCanvasRef.value
|
||
const ctx = canvas?.getContext('2d')
|
||
if (!canvas || !ctx) return
|
||
canvas.setPointerCapture(e.pointerId)
|
||
signatureActivePointerId.value = e.pointerId
|
||
const { x, y } = getSignatureLocalPoint(e)
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, y)
|
||
}
|
||
|
||
function onSignaturePointerMove(e: PointerEvent) {
|
||
if (signatureActivePointerId.value !== e.pointerId) return
|
||
const ctx = signatureCanvasRef.value?.getContext('2d')
|
||
if (!ctx) return
|
||
const { x, y } = getSignatureLocalPoint(e)
|
||
ctx.lineTo(x, y)
|
||
ctx.stroke()
|
||
ctx.beginPath()
|
||
ctx.moveTo(x, y)
|
||
}
|
||
|
||
function onSignaturePointerUp(e: PointerEvent) {
|
||
if (signatureActivePointerId.value !== e.pointerId) return
|
||
signatureActivePointerId.value = null
|
||
const canvas = signatureCanvasRef.value
|
||
if (canvas) {
|
||
try {
|
||
canvas.releasePointerCapture(e.pointerId)
|
||
} catch {
|
||
/* 已释放 */
|
||
}
|
||
}
|
||
commitSignatureImage()
|
||
}
|
||
|
||
function clearSignature() {
|
||
initSignatureCanvas()
|
||
editForm.doctor_signature = ''
|
||
nextTick(() => {
|
||
formRef.value?.validateField('doctor_signature').catch(() => {})
|
||
})
|
||
}
|
||
|
||
function initSignatureCanvas() {
|
||
const canvas = signatureCanvasRef.value
|
||
if (!canvas) return
|
||
const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, 2.5)
|
||
canvas.style.width = `${SIGNATURE_CSS_W}px`
|
||
canvas.style.height = `${SIGNATURE_CSS_H}px`
|
||
canvas.width = Math.round(SIGNATURE_CSS_W * dpr)
|
||
canvas.height = Math.round(SIGNATURE_CSS_H * dpr)
|
||
const ctx = canvas.getContext('2d')
|
||
if (!ctx) return
|
||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||
ctx.scale(dpr, dpr)
|
||
ctx.fillStyle = '#fff'
|
||
ctx.fillRect(0, 0, SIGNATURE_CSS_W, SIGNATURE_CSS_H)
|
||
ctx.strokeStyle = '#333'
|
||
ctx.lineWidth = 2.5
|
||
ctx.lineCap = 'round'
|
||
ctx.lineJoin = 'round'
|
||
}
|
||
|
||
/** 抽屉打开后初始化画布;编辑时若有已存签名则绘制到画布 */
|
||
function scheduleEditSignatureInit() {
|
||
if (editMode.value === 'view') return
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
initSignatureCanvas()
|
||
const sig = editForm.doctor_signature
|
||
if (sig && String(sig).trim().startsWith('data:image') && signatureCanvasRef.value) {
|
||
const img = new Image()
|
||
img.onload = () => {
|
||
const canvas = signatureCanvasRef.value
|
||
const ctx = canvas?.getContext('2d')
|
||
if (!canvas || !ctx) return
|
||
ctx.drawImage(img, 0, 0, SIGNATURE_CSS_W, SIGNATURE_CSS_H)
|
||
commitSignatureImage()
|
||
}
|
||
img.onerror = () => {
|
||
commitSignatureImage()
|
||
}
|
||
img.src = sig
|
||
}
|
||
}, 100)
|
||
})
|
||
}
|
||
|
||
function formatVisibleRoleNames(ids: number[] | undefined) {
|
||
if (!ids || !ids.length) return '—'
|
||
const names = ids
|
||
.map((id) => roleOptions.value.find((r) => r.id === id)?.name)
|
||
.filter(Boolean)
|
||
return names.length ? names.join('、') : ids.join('、')
|
||
}
|
||
|
||
function normalizeVisibleRoleIds(row: any): number[] {
|
||
const v = row?.visible_role_ids
|
||
if (Array.isArray(v)) return v.map((n) => Number(n)).filter((n) => !Number.isNaN(n))
|
||
if (typeof v === 'string' && v)
|
||
return v
|
||
.split(',')
|
||
.map((s) => parseInt(s.trim(), 10))
|
||
.filter((n) => !Number.isNaN(n))
|
||
return []
|
||
}
|
||
|
||
function openAuditDialog(row: any) {
|
||
auditTargetId.value = row.id
|
||
auditRemark.value = ''
|
||
auditDialogVisible.value = true
|
||
}
|
||
|
||
async function submitAudit(action: 'approve' | 'reject') {
|
||
if (action === 'reject' && !auditRemark.value.trim()) {
|
||
feedback.msgError('驳回时请填写审核意见')
|
||
return
|
||
}
|
||
auditLoading.value = true
|
||
try {
|
||
const auditData = (await prescriptionAudit({
|
||
id: auditTargetId.value,
|
||
action,
|
||
remark: auditRemark.value.trim()
|
||
})) as { wecom_notify_ok?: boolean; wecom_notify_hint?: string } | void
|
||
feedback.msgSuccess(action === 'reject' ? '已驳回并作废处方' : '审核通过')
|
||
if (auditData && auditData.wecom_notify_ok === false && auditData.wecom_notify_hint) {
|
||
feedback.msgWarning(auditData.wecom_notify_hint)
|
||
}
|
||
auditDialogVisible.value = false
|
||
getLists()
|
||
} catch {
|
||
/* 拦截器已提示 */
|
||
} finally {
|
||
auditLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadRoleOptions() {
|
||
try {
|
||
const list = await roleAll({})
|
||
roleOptions.value = Array.isArray(list) ? list : []
|
||
} catch {
|
||
roleOptions.value = []
|
||
}
|
||
}
|
||
|
||
// —— 处方库导入(与 components/tcm-prescription 一致,按 creator_id 拉取该医师库内处方)——
|
||
const showLibraryDialog = ref(false)
|
||
const libraryLoading = ref(false)
|
||
const libraryList = ref<any[]>([])
|
||
const librarySearchName = ref('')
|
||
const libraryPage = ref(1)
|
||
const libraryPageSize = ref(15)
|
||
const libraryTotal = ref(0)
|
||
|
||
/** 当前用于检索处方库的开方医师 ID:编辑用处方 creator_id,新增用当前登录账号 */
|
||
const libraryDoctorId = computed(() => {
|
||
const cid = Number(editForm.creator_id)
|
||
if (cid > 0) return cid
|
||
const uid = Number(userStore.userInfo?.id)
|
||
return uid > 0 ? uid : 0
|
||
})
|
||
|
||
function normalizeHerbNameLocal(s: string) {
|
||
return String(s || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
}
|
||
|
||
function findDuplicateHerbNamesLocal(): string[] {
|
||
const map = new Map<string, number>()
|
||
for (const h of editForm.herbs) {
|
||
const key = normalizeHerbNameLocal(h?.name)
|
||
if (!key) continue
|
||
map.set(key, (map.get(key) ?? 0) + 1)
|
||
}
|
||
return [...map.entries()]
|
||
.filter(([, c]) => c > 1)
|
||
.map(([name]) => name)
|
||
}
|
||
|
||
const loadLibraryList = async () => {
|
||
const doctorId = libraryDoctorId.value
|
||
if (!doctorId) {
|
||
feedback.msgWarning('无法确定开方医师,无法加载处方库')
|
||
libraryList.value = []
|
||
libraryTotal.value = 0
|
||
return
|
||
}
|
||
libraryLoading.value = true
|
||
try {
|
||
const res: any = await prescriptionLibraryLists({
|
||
page_no: libraryPage.value,
|
||
page_size: libraryPageSize.value,
|
||
prescription_name: librarySearchName.value,
|
||
is_public: '',
|
||
creator_id: doctorId
|
||
})
|
||
libraryList.value = res?.lists || []
|
||
libraryTotal.value = res?.count || 0
|
||
} catch (e) {
|
||
console.error(e)
|
||
feedback.msgError('加载处方库失败')
|
||
} finally {
|
||
libraryLoading.value = false
|
||
}
|
||
}
|
||
|
||
const searchLibrary = () => {
|
||
libraryPage.value = 1
|
||
loadLibraryList()
|
||
}
|
||
|
||
const openLibraryDialog = () => {
|
||
const doctorId = libraryDoctorId.value
|
||
if (!doctorId) {
|
||
feedback.msgWarning('无法确定开方医师,无法从处方库导入')
|
||
return
|
||
}
|
||
showLibraryDialog.value = true
|
||
}
|
||
|
||
const handleSelectLibraryRow = (row: any) => {
|
||
handleImportLibrary(row)
|
||
}
|
||
|
||
const handleImportLibrary = (row: any) => {
|
||
if (!row.herbs || row.herbs.length === 0) {
|
||
feedback.msgWarning('该处方没有药材信息')
|
||
return
|
||
}
|
||
const imported = JSON.parse(JSON.stringify(row.herbs)) as Array<{ name: string; dosage: number }>
|
||
editForm.herbs = imported
|
||
if (findDuplicateHerbNamesLocal().length) {
|
||
feedback.msgWarning('导入的处方中存在重复药名,请合并剂量或删除多余行')
|
||
}
|
||
feedback.msgSuccess(`已导入处方「${row.prescription_name}」,共${imported.length}味药材`)
|
||
showLibraryDialog.value = false
|
||
}
|
||
|
||
watch(showLibraryDialog, (open) => {
|
||
if (open) {
|
||
librarySearchName.value = ''
|
||
libraryPage.value = 1
|
||
loadLibraryList()
|
||
}
|
||
})
|
||
|
||
// 添加药材
|
||
const addHerb = () => {
|
||
editForm.herbs.push({
|
||
name: '',
|
||
dosage: 0
|
||
})
|
||
}
|
||
|
||
// 删除药材
|
||
const removeHerb = (index: number) => {
|
||
editForm.herbs.splice(index, 1)
|
||
}
|
||
|
||
// 重置表单
|
||
const resetForm = () => {
|
||
editForm.id = 0
|
||
editForm.prescription_type = '浓缩水丸'
|
||
editForm.patient_name = ''
|
||
editForm.gender = 1
|
||
editForm.age = 0
|
||
editForm.visit_no = ''
|
||
editForm.prescription_date = new Date().toISOString().split('T')[0]
|
||
editForm.tongue = ''
|
||
editForm.tongue_image = ''
|
||
editForm.pulse = ''
|
||
editForm.pulse_condition = ''
|
||
editForm.clinical_diagnosis = ''
|
||
editForm.herbs = []
|
||
editForm.dose_count = 7
|
||
editForm.dose_unit = '剂'
|
||
editForm.usage_days = 7
|
||
editForm.usage_instruction = ''
|
||
editForm.usage_time = '饭后'
|
||
editForm.usage_way = '温水送服'
|
||
editForm.dietary_taboo = []
|
||
editForm.usage_notes = ''
|
||
editForm.doctor_name = ''
|
||
editForm.doctor_signature = ''
|
||
editForm.is_shared = 0
|
||
editForm.visible_role_ids = []
|
||
editForm.audit_status = 1
|
||
editForm.audit_time = null
|
||
editForm.audit_by_name = ''
|
||
editForm.audit_remark = ''
|
||
editForm.diagnosis_id = 0
|
||
editForm.creator_id = Number(userStore.userInfo?.id) || 0
|
||
editForm.is_system_auto = 0
|
||
editForm.business_prescription_audit_rejected = 0
|
||
editForm.business_prescription_audit_remark = ''
|
||
}
|
||
|
||
// 新增处方
|
||
const handleAdd = () => {
|
||
resetForm()
|
||
editForm.doctor_name = String(userStore.userInfo?.name || '')
|
||
editingWasVoid.value = false
|
||
editingWasRejected.value = false
|
||
editMode.value = 'add'
|
||
// 新建时允许watch自动设置默认值
|
||
isLoadingData.value = false
|
||
showEdit.value = true
|
||
nextTick(() => {
|
||
formRef.value?.clearValidate()
|
||
scheduleEditSignatureInit()
|
||
})
|
||
}
|
||
|
||
// 查看处方(处方笺样式,详情接口补全电话、签名等)
|
||
const handleView = async (row: any) => {
|
||
editingWasVoid.value = false
|
||
editingWasRejected.value = false
|
||
editMode.value = 'view'
|
||
slipView.value = rowToSlipView(row)
|
||
showEdit.value = true
|
||
viewDetailLoading.value = true
|
||
try {
|
||
const d = await prescriptionDetail({ id: row.id })
|
||
if (d && typeof d === 'object') {
|
||
slipView.value = {
|
||
...d,
|
||
herbs: normalizeSlipHerbs((d as any).herbs)
|
||
}
|
||
}
|
||
} catch (e: any) {
|
||
feedback.msgError(e?.msg || '加载处方详情失败')
|
||
} finally {
|
||
viewDetailLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 编辑处方
|
||
const handleEdit = async (row: any) => {
|
||
editingWasVoid.value = Number(row.void_status) === 1
|
||
editingWasRejected.value = Number(row.audit_status) === 2
|
||
editMode.value = 'edit'
|
||
|
||
// 开始加载数据,暂时禁用watch自动重置
|
||
isLoadingData.value = true
|
||
|
||
let src = row
|
||
if (row?.id && !row.doctor_signature) {
|
||
try {
|
||
const d = await prescriptionDetail({ id: row.id })
|
||
if (d && typeof d === 'object') {
|
||
src = { ...row, ...d }
|
||
}
|
||
} catch {
|
||
/* 保持列表行 */
|
||
}
|
||
}
|
||
|
||
// 深拷贝数据到表单
|
||
editForm.id = src.id
|
||
editForm.prescription_type = src.prescription_type || '浓缩水丸'
|
||
|
||
// 确保 dosage_amount 是数字类型,以匹配下拉框的 :value
|
||
if (src.dosage_amount !== null && src.dosage_amount !== undefined && src.dosage_amount !== '') {
|
||
editForm.dosage_amount = Number(src.dosage_amount)
|
||
} else {
|
||
editForm.dosage_amount = undefined
|
||
}
|
||
|
||
// 如果数据库中没有 dosage_unit,根据 prescription_type 设置默认值
|
||
if (src.dosage_unit) {
|
||
editForm.dosage_unit = src.dosage_unit
|
||
} else {
|
||
if (editForm.prescription_type === '饮片') {
|
||
editForm.dosage_unit = 'ml'
|
||
} else {
|
||
editForm.dosage_unit = 'g'
|
||
}
|
||
}
|
||
|
||
editForm.need_decoction = src.need_decoction === 1 || src.need_decoction === true
|
||
editForm.bags_per_dose = src.bags_per_dose ? Number(src.bags_per_dose) : 1
|
||
editForm.patient_name = src.patient_name || ''
|
||
editForm.gender = src.gender ?? 1
|
||
editForm.age = src.age ?? 0
|
||
editForm.visit_no = src.visit_no || ''
|
||
editForm.prescription_date = src.prescription_date || ''
|
||
editForm.tongue = src.tongue || ''
|
||
editForm.tongue_image = src.tongue_image || ''
|
||
editForm.pulse = src.pulse || ''
|
||
editForm.pulse_condition = src.pulse_condition || ''
|
||
editForm.clinical_diagnosis = src.clinical_diagnosis || ''
|
||
editForm.herbs = src.herbs ? JSON.parse(JSON.stringify(src.herbs)) : []
|
||
editForm.dose_count = src.dose_count ?? 7
|
||
editForm.dose_unit = src.dose_unit || '剂'
|
||
editForm.usage_days = src.usage_days ?? 7
|
||
editForm.times_per_day = src.times_per_day ?? 2
|
||
editForm.usage_instruction = src.usage_instruction || ''
|
||
editForm.usage_time = src.usage_time || '饭后'
|
||
editForm.usage_way = src.usage_way || '温水送服'
|
||
editForm.dietary_taboo = src.dietary_taboo
|
||
? (Array.isArray(src.dietary_taboo) ? src.dietary_taboo : String(src.dietary_taboo).split(',').filter((v: string) => v))
|
||
: []
|
||
editForm.usage_notes = src.usage_notes || ''
|
||
editForm.doctor_name = src.doctor_name || ''
|
||
editForm.doctor_signature = String(src.doctor_signature || '')
|
||
editForm.is_shared = src.is_shared ?? 0
|
||
editForm.visible_role_ids = normalizeVisibleRoleIds(src)
|
||
editForm.audit_status = src.audit_status ?? 1
|
||
editForm.audit_time = src.audit_time ?? null
|
||
editForm.audit_by_name = src.audit_by_name || ''
|
||
editForm.audit_remark = src.audit_remark || ''
|
||
editForm.diagnosis_id = src.diagnosis_id ?? 0
|
||
editForm.creator_id = Number(src.creator_id) > 0 ? Number(src.creator_id) : 0
|
||
editForm.is_system_auto = Number(src.is_system_auto) === 1 ? 1 : 0
|
||
editForm.business_prescription_audit_rejected = Number(src.business_prescription_audit_rejected) === 1 ? 1 : 0
|
||
editForm.business_prescription_audit_remark = String(src.business_prescription_audit_remark || '')
|
||
|
||
// 数据加载完成,重新启用watch
|
||
// 使用 setTimeout 确保所有数据都已经渲染完成
|
||
setTimeout(() => {
|
||
isLoadingData.value = false
|
||
}, 0)
|
||
|
||
showEdit.value = true
|
||
nextTick(() => {
|
||
formRef.value?.clearValidate()
|
||
scheduleEditSignatureInit()
|
||
})
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate()
|
||
|
||
// 验证药材
|
||
if (!editForm.herbs || editForm.herbs.length === 0) {
|
||
feedback.msgError('请至少添加一味药材')
|
||
return
|
||
}
|
||
|
||
for (let i = 0; i < editForm.herbs.length; i++) {
|
||
const herb = editForm.herbs[i]
|
||
if (!herb.name || !herb.name.trim()) {
|
||
feedback.msgError(`第${i + 1}味药材名称不能为空`)
|
||
return
|
||
}
|
||
if (!herb.dosage || herb.dosage <= 0) {
|
||
feedback.msgError(`第${i + 1}味药材剂量必须大于0`)
|
||
return
|
||
}
|
||
}
|
||
|
||
submitLoading.value = true
|
||
|
||
try {
|
||
const params: Record<string, unknown> = { ...editForm }
|
||
|
||
if (editMode.value === 'add') {
|
||
params.audit_status = 0
|
||
await prescriptionAdd(params)
|
||
feedback.msgSuccess('添加成功,已提交审核')
|
||
} else {
|
||
await prescriptionEdit(params)
|
||
feedback.msgSuccess('保存成功,处方已重新进入待审核')
|
||
}
|
||
|
||
showEdit.value = false
|
||
getLists()
|
||
} catch (error) {
|
||
console.error(error)
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 删除处方
|
||
const handleDelete = async (id: number) => {
|
||
await feedback.confirm('确定要删除该处方吗?')
|
||
await prescriptionDelete({ id })
|
||
feedback.msgSuccess('删除成功')
|
||
getLists()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadRoleOptions()
|
||
await loadRegionData()
|
||
await loadServicePackageOptions()
|
||
try {
|
||
const list = await getDoctors()
|
||
doctorOptions.value = Array.isArray(list) ? list : []
|
||
} catch {
|
||
doctorOptions.value = []
|
||
}
|
||
try {
|
||
if (!userStore.userInfo?.id) {
|
||
await userStore.getUserInfo()
|
||
}
|
||
} catch {
|
||
/* 已登录页仍可无用户信息,保持默认筛选 */
|
||
}
|
||
getLists()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.prescription-list {
|
||
padding: 12px 16px 20px;
|
||
}
|
||
|
||
/* 紧凑检索条:少占纵向空间,表格才是主体 */
|
||
.filter-toolbar {
|
||
border-radius: 8px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
background: var(--el-fill-color-blank);
|
||
}
|
||
|
||
.pl-toolbar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pl-toolbar__line {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 10px;
|
||
}
|
||
|
||
.pl-toolbar__line--second {
|
||
padding-top: 2px;
|
||
border-top: 1px dashed var(--el-border-color-lighter);
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.pl-toolbar__k {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-secondary);
|
||
line-height: 1;
|
||
}
|
||
|
||
.pl-toolbar__sep {
|
||
width: 1px;
|
||
height: 18px;
|
||
background: var(--el-border-color);
|
||
opacity: 0.7;
|
||
margin: 0 2px;
|
||
}
|
||
|
||
.pl-toolbar__rg {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.pl-toolbar__rg :deep(.el-radio-button__inner) {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 审核人员最常用:待审核 */
|
||
.pl-toolbar__rg--audit :deep(.pl-audit-pending-btn .el-radio-button__inner) {
|
||
font-weight: 600;
|
||
padding: 6px 14px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.pl-toolbar__rg--audit :deep(.pl-audit-pending-btn:not(.is-active) .el-radio-button__inner) {
|
||
color: var(--el-color-primary);
|
||
border-color: var(--el-color-primary-light-5);
|
||
background: var(--el-color-primary-light-9);
|
||
}
|
||
|
||
.pl-toolbar__rg--audit :deep(.pl-audit-pending-btn.is-active .el-radio-button__inner) {
|
||
background: var(--el-color-primary) !important;
|
||
border-color: var(--el-color-primary) !important;
|
||
color: #fff !important;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.pl-toolbar__range {
|
||
flex: 1 1 240px;
|
||
min-width: 220px;
|
||
max-width: 380px;
|
||
}
|
||
|
||
.pl-toolbar__range :deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
|
||
.pl-toolbar__range :deep(.el-date-editor.el-input__wrapper) {
|
||
min-height: 30px;
|
||
padding: 2px 8px;
|
||
}
|
||
|
||
.pl-toolbar__form {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0 4px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.pl-toolbar__form :deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.pl-toolbar__form :deep(.el-form-item__label) {
|
||
font-size: 12px;
|
||
padding-right: 6px;
|
||
}
|
||
|
||
.pl-toolbar__input {
|
||
width: 132px;
|
||
}
|
||
|
||
.pl-toolbar__select {
|
||
width: 108px;
|
||
}
|
||
|
||
.pl-toolbar__select--doctors {
|
||
width: min(320px, 100%);
|
||
min-width: 200px;
|
||
}
|
||
|
||
.pl-toolbar__actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.table-card {
|
||
border-radius: 8px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.pl-table-head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.pl-table-head__title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
/* 查看处方:互联网医院处方笺(对齐诊间 tcm-prescription 打印版式) */
|
||
.cf-prescription-drawer :deep(.el-drawer__body) {
|
||
padding-top: 8px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 处方笺(A4 药房联)样式
|
||
* 严格按 A4:210mm × 297mm,padding 12mm
|
||
* ============================================================ */
|
||
.rx-wrap {
|
||
min-height: 160px;
|
||
background: #f5f6f8;
|
||
padding: 12px;
|
||
margin: -20px;
|
||
}
|
||
|
||
.rx-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: #fff;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 10px;
|
||
|
||
.flex-1 { flex: 1; }
|
||
}
|
||
|
||
.rx-status {
|
||
margin-bottom: 10px;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.rx-status-danger {
|
||
background: #fef0f0;
|
||
color: #b91c1c;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
/* === A4 纸 === */
|
||
.rx-paper {
|
||
width: 210mm;
|
||
min-height: 297mm;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
box-sizing: border-box;
|
||
padding: 8mm 10mm;
|
||
color: #1f1f1f;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
font-family: "Microsoft YaHei", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
box-shadow: 0 0 0 1px #d6d6d6, 0 6px 24px rgba(0, 0, 0, 0.06);
|
||
/* 防止 html2canvas 把内部表格裁切 */
|
||
overflow: visible;
|
||
}
|
||
|
||
/* === 顶部告知 === */
|
||
.rx-notice {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f3f4f6;
|
||
border: 1px solid #e5e7eb;
|
||
padding: 6px 10px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
color: #1f1f1f;
|
||
margin-bottom: 6px;
|
||
|
||
.rx-notice-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.rx-notice-meta {
|
||
display: inline-flex;
|
||
gap: 24px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
/* === 患者信息表 === */
|
||
.rx-info {
|
||
border: 1px solid #c8c8c8;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.rx-info-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
border-bottom: 1px solid #c8c8c8;
|
||
}
|
||
|
||
.rx-info-row-full {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.rx-info-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
font-size: 13px;
|
||
border-right: 1px solid #c8c8c8;
|
||
min-height: 32px;
|
||
|
||
&:last-child { border-right: none; }
|
||
|
||
.rx-key {
|
||
color: #1f1f1f;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.rx-val {
|
||
flex: 1;
|
||
min-width: 0;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
/* === Rp 区域 + 水印 === */
|
||
.rx-rp {
|
||
position: relative;
|
||
border-left: 1px solid #c8c8c8;
|
||
border-right: 1px solid #c8c8c8;
|
||
padding: 8px 10px 16px;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
}
|
||
|
||
.rx-watermark {
|
||
position: absolute;
|
||
/* 不使用 inset 简写:html2canvas 对 inset 解析不稳定,
|
||
* 会导致水印容器尺寸异常,进而影响内层网格对齐 */
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 88px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.06);
|
||
transform: rotate(-22deg);
|
||
letter-spacing: 0.18em;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.rx-rp-head {
|
||
display: grid;
|
||
grid-template-columns: 36px 1fr;
|
||
align-items: end;
|
||
column-gap: 8px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid #d4d4d4;
|
||
margin-bottom: 6px;
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rx-rp-label {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #1f1f1f;
|
||
line-height: 1;
|
||
}
|
||
|
||
.rx-rp-cols-head {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
column-gap: 24px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* 注意:列内 1fr 64px 与 .rx-herb-cell 完全一致,
|
||
* 配合 .rx-herbs 与 .rx-rp-cols-head 同样的左侧 44px 缩进,
|
||
* 即可保证「用药 (单剂)」与药材名、「总量」与总量数字精确对齐。 */
|
||
.rx-rp-col-head {
|
||
display: grid;
|
||
grid-template-columns: 1fr 64px;
|
||
column-gap: 8px;
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
min-width: 0;
|
||
|
||
& > span:last-child {
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.rx-herbs {
|
||
/* 与 .rx-rp-head 中 Rp. 列(36px) + column-gap(8px) 等宽缩进,
|
||
* 这样下方两列的左右边界与表头两列完全对齐。 */
|
||
padding-left: 44px;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
column-gap: 24px;
|
||
row-gap: 6px;
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rx-herb-cell {
|
||
display: grid;
|
||
grid-template-columns: 1fr 64px;
|
||
column-gap: 8px;
|
||
font-size: 13px;
|
||
line-height: 1.85;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rx-herb-name {
|
||
color: #1f1f1f;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rx-herb-total {
|
||
color: #1f1f1f;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* === 服法 / 备注 文本块 === */
|
||
.rx-text {
|
||
border: 1px solid #c8c8c8;
|
||
border-top: none;
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
line-height: 1.85;
|
||
|
||
p { margin: 0; }
|
||
|
||
.rx-text-warn {
|
||
color: #d72424;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
/* === 底部表格 === */
|
||
.rx-bottom {
|
||
border: 1px solid #c8c8c8;
|
||
border-top: none;
|
||
}
|
||
|
||
.rx-bot-row {
|
||
display: grid;
|
||
border-bottom: 1px solid #c8c8c8;
|
||
|
||
&:last-child { border-bottom: none; }
|
||
}
|
||
|
||
.rx-bot-row-1 {
|
||
/* 类型/剂数/单剂量 三个 meta 列适当加宽,
|
||
* 避免"浓缩丸-浓缩水丸"被强制折行后把整行挤错位 */
|
||
grid-template-columns: 1.4fr 0.6fr 0.6fr 0.6fr 0.6fr 1.7fr 0.95fr 1.25fr;
|
||
min-height: 70px;
|
||
}
|
||
|
||
.rx-bot-row-2 {
|
||
grid-template-columns: repeat(6, 1fr);
|
||
}
|
||
|
||
/* 默认:单行内容左对齐、上下居中(适配 类型/剂数/单剂量/费用 等格子)。
|
||
* 需要"标题置顶 + 签名留白"的格子(医师/审方/配药/复核/发药)请加 .rx-bot-stack。
|
||
* 这样无需任何 !important 反向覆盖,html2canvas/原生渲染表现一致。 */
|
||
.rx-bot-cell {
|
||
border-right: 1px solid #c8c8c8;
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
color: #1f1f1f;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
min-height: 32px;
|
||
box-sizing: border-box;
|
||
|
||
&:last-child { border-right: none; }
|
||
}
|
||
|
||
.rx-bot-stack {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.rx-bot-label {
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.rx-bot-doctor {
|
||
.rx-bot-doctor-body {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.rx-bot-doctor-name {
|
||
font-size: 13px;
|
||
color: #1f1f1f;
|
||
}
|
||
|
||
.rx-bot-sign-img {
|
||
max-height: 40px;
|
||
max-width: 110px;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.rx-bot-meta {
|
||
flex-wrap: nowrap;
|
||
|
||
.rx-bot-meta-key {
|
||
color: #1f1f1f;
|
||
margin-right: 6px;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.rx-bot-meta-val {
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
min-width: 0;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
.rx-fee {
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
padding: 8px 10px;
|
||
|
||
.rx-fee-key {
|
||
color: #1f1f1f;
|
||
margin-right: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.rx-fee-val {
|
||
font-variant-numeric: tabular-nums;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.rx-fee-total {
|
||
font-weight: 700;
|
||
background: #fafafa;
|
||
|
||
.rx-fee-val { font-weight: 700; }
|
||
}
|
||
|
||
/* === 审核痕迹(不打印) === */
|
||
.rx-audit {
|
||
margin-top: 12px;
|
||
padding: 8px 12px;
|
||
background: #fafafa;
|
||
border: 1px dashed #d4d4d4;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* 打印控制规则放到非 scoped 的全局 style 中,参见底部 <style> */
|
||
|
||
/* 创建业务订单弹窗:宽松留白、默认字号、卡片化信息区 */
|
||
.create-order-dialog {
|
||
:deep(.el-dialog__header) {
|
||
padding: 20px 24px 12px;
|
||
margin-right: 0;
|
||
}
|
||
|
||
:deep(.el-dialog__title) {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
:deep(.el-dialog__body) {
|
||
padding: 8px 24px 20px;
|
||
max-height: calc(100vh - 200px);
|
||
overflow: hidden;
|
||
}
|
||
|
||
:deep(.el-dialog__footer) {
|
||
padding: 16px 24px 20px;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
background: var(--el-fill-color-blank);
|
||
}
|
||
}
|
||
|
||
.create-order-dialog__inner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
}
|
||
|
||
.create-order-rx-card {
|
||
border-radius: 10px;
|
||
border: 1px solid var(--el-color-primary-light-5);
|
||
background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-blank) 100%);
|
||
padding: 16px 18px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
|
||
}
|
||
|
||
.create-order-rx-card__head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px 14px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.create-order-rx-card__badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
background: var(--el-color-primary);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.create-order-rx-card__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 12px;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.create-order-rx-card__id {
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.create-order-rx-card__sn {
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
background: var(--el-fill-color);
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.create-order-rx-card__patient {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.create-order-rx-card__herbs {
|
||
font-size: 13px;
|
||
line-height: 1.55;
|
||
color: var(--el-text-color-secondary);
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
cursor: default;
|
||
padding-top: 2px;
|
||
}
|
||
|
||
.create-order-steps {
|
||
margin-bottom: 8px;
|
||
padding: 8px 12px 4px;
|
||
|
||
:deep(.el-step__title) {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
:deep(.el-step__description) {
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
line-height: 1.35;
|
||
padding-right: 6px;
|
||
}
|
||
|
||
:deep(.el-step__head) {
|
||
padding-right: 8px;
|
||
}
|
||
|
||
:deep(.el-step.is-horizontal .el-step__line) {
|
||
top: 14px;
|
||
}
|
||
}
|
||
|
||
.create-order-step-lead {
|
||
margin: 0 0 20px;
|
||
padding: 12px 16px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: var(--el-text-color-secondary);
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 8px;
|
||
border-left: 3px solid var(--el-color-primary-light-5);
|
||
}
|
||
|
||
.create-order-step-panel {
|
||
max-height: min(56vh, 480px);
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 4px 8px 12px 4px;
|
||
margin-right: -4px;
|
||
}
|
||
|
||
.create-order-step-panel__row-after-bar {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.create-order-unit-suffix {
|
||
flex-shrink: 0;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
padding-left: 4px;
|
||
}
|
||
|
||
.create-order-footer {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
width: 100%;
|
||
}
|
||
|
||
.create-order-footer__hint {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.create-order-footer__step {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.create-order-footer__warn {
|
||
color: var(--el-color-warning);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.create-order-footer__actions {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.create-order-deposit-bar {
|
||
display: flex;
|
||
align-items: stretch;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
overflow: hidden;
|
||
font-size: 13px;
|
||
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
|
||
}
|
||
|
||
.create-order-deposit-bar__cell {
|
||
flex: 1;
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
background: var(--el-fill-color-light);
|
||
|
||
strong {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-primary);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
}
|
||
|
||
.create-order-deposit-bar__label {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.create-order-deposit-bar__sub {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.create-order-deposit-bar__sep {
|
||
width: 1px;
|
||
background: var(--el-border-color-lighter);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.create-order-deposit-bar.is-ok .create-order-deposit-bar__cell:last-child strong {
|
||
color: var(--el-color-success);
|
||
}
|
||
|
||
.create-order-deposit-bar.is-warn .create-order-deposit-bar__cell:last-child strong {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.create-order-pay-hint {
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
margin-top: 8px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.create-order-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
font-size: 14px;
|
||
padding-right: 12px;
|
||
}
|
||
|
||
:deep(.el-input__inner),
|
||
:deep(.el-textarea__inner) {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.create-order-form-item--inline-num {
|
||
:deep(.el-form-item__content) {
|
||
line-height: normal;
|
||
}
|
||
}
|
||
|
||
.signature-pad-wrap {
|
||
width: 100%;
|
||
max-width: 600px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
.signature-canvas {
|
||
display: block;
|
||
cursor: crosshair;
|
||
touch-action: none;
|
||
}
|
||
|
||
.signature-actions {
|
||
padding: 8px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<!-- 全局打印样式(不能 scoped,否则 body * 不生效) -->
|
||
<style>
|
||
@media print {
|
||
body * { visibility: hidden !important; }
|
||
|
||
.rx-paper, .rx-paper * { visibility: visible !important; }
|
||
|
||
.rx-paper {
|
||
position: fixed !important;
|
||
left: 0 !important;
|
||
top: 0 !important;
|
||
width: 210mm !important;
|
||
height: 297mm !important;
|
||
margin: 0 !important;
|
||
padding: 8mm 10mm !important;
|
||
box-shadow: none !important;
|
||
page-break-after: always;
|
||
}
|
||
|
||
@page {
|
||
size: A4;
|
||
margin: 0;
|
||
}
|
||
}
|
||
</style>
|