Files
zyt/admin/src/views/order/index.vue
T
2026-05-11 11:25:03 +08:00

1576 lines
60 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 订单列表 -->
<template>
<div class="order-list">
<el-card class="!border-none" shadow="never">
<!-- 搜索表单 -->
<el-form class="ls-form" :model="queryParams" inline>
<el-form-item class="w-[280px]" label="订单号">
<el-input
v-model="queryParams.order_no"
placeholder="请输入订单号"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="患者信息">
<el-input
v-model="queryParams.patient_keyword"
placeholder="患者姓名/手机号"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="医助">
<el-select
v-model="queryParams.assistant_id"
placeholder="全部医助"
clearable
filterable
class="w-full"
>
<el-option
v-for="item in assistantOptions"
:key="item.id"
:label="item.name"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item class="w-[280px]" label="订单类型">
<el-select v-model="queryParams.order_type" class="w-full" placeholder="全部">
<el-option label="全部" value="" />
<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-option label="驼奶费用" :value="8" />
</el-select>
</el-form-item>
<el-form-item class="w-[280px]" label="订单状态">
<el-select v-model="queryParams.status" class="w-full" placeholder="全部">
<el-option label="全部" value="" />
<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-select>
</el-form-item>
<el-form-item label="创建时间">
<daterange-picker
v-model:startTime="queryParams.create_time_start"
v-model:endTime="queryParams.create_time_end"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button
v-perms="['order.order/lists']"
@click="statsDialogVisible = true"
>
操作统计
</el-button>
<export-data
v-perms="['order.order/export']"
class="ml-2.5"
:fetch-fun="orderExport"
:params="queryParams"
:page-size="pager.size"
/>
<el-button
v-perms="['order.order/create']"
type="success"
class="ml-2.5"
@click="handleCreate"
>
+ 创建支付单
</el-button>
<el-button
v-perms="['order.order/alltongbu']"
type="warning"
class="ml-2.5"
:loading="syncLoading"
@click="handleSyncWechatWork"
>
同步企业微信收款
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- Tab 筛选 + 今日收益 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="flex items-center justify-between mb-4">
<el-tabs v-model="patientAssociationTab" @tab-change="handleTabChange">
<el-tab-pane label="全部" name="" />
<el-tab-pane label="待关联" name="pending" />
<el-tab-pane label="已关联" name="associated" />
</el-tabs>
<div class="flex items-center gap-4 text-sm">
<span class="text-gray-500">今日收益</span>
<span class="text-red-500 font-semibold text-lg">¥{{ todayRevenue.amount }}</span>
<span class="text-gray-400">{{ todayRevenue.count }} </span>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card class="!border-none mt-4" shadow="never">
<div v-perms="['order.order/zhipai']" class="mb-3 flex items-center gap-3">
<el-button type="primary" :disabled="!selectedOrderIds.length" @click="openAssignDialog">
将创建人指给医助
</el-button>
<span v-if="selectedOrderIds.length" class="text-sm text-gray-500"
>已选 {{ selectedOrderIds.length }} </span
>
</div>
<el-table
ref="orderTableRef"
v-loading="pager.loading"
:data="pager.lists"
row-key="id"
size="large"
@selection-change="handleOrderSelectionChange"
>
<el-table-column type="selection" width="48" />
<el-table-column label="订单号" prop="order_no" min-width="150" />
<el-table-column label="患者信息" min-width="150">
<template #default="{ row }">
<div v-if="row.patient">{{ row.patient.patient_name }}</div>
<div v-else class="text-amber-600">待关联</div>
<div class="text-xs text-gray-400">{{ row.patient?.phone || '-' }}</div>
</template>
</el-table-column>
<el-table-column label="创建人" min-width="120">
<template #default="{ row }">
<div>{{ row.creator?.name || '-' }}</div>
<div v-if="row.payee_userid" class="text-xs text-gray-400">收款: {{ row.payee_userid }}</div>
</template>
</el-table-column>
<el-table-column label="订单类型" width="100">
<template #default="{ row }">
<el-tag :type="getOrderTypeTag(row.order_type)">
{{ getOrderTypeText(row.order_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="金额" width="100">
<template #default="{ row }">
<span class="text-red-500 font-semibold">¥{{ row.amount }}</span>
</template>
</el-table-column>
<el-table-column label="支付方式" width="100">
<template #default="{ row }">
{{ getPaymentMethodText(row.payment_method) }}
</template>
</el-table-column>
<el-table-column label="订单状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusTag(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
<el-tag v-if="row.is_exempt === 1" type="warning" size="small" class="ml-1">豁免</el-tag>
</template>
</el-table-column>
<el-table-column label="支付时间" min-width="160">
<template #default="{ row }">
{{ row.payment_time || '-' }}
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="160">
<template #default="{ row }">
{{ row.create_time }}
</template>
</el-table-column>
<el-table-column label="操作" width="340" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['order.order/detail']"
type="primary"
link
@click="handleDetail(row)"
>
详情
</el-button>
<el-button
v-perms="['order.order/edit']"
type="primary"
link
@click="handleEditOrder(row)"
>
编辑
</el-button>
<el-button
v-if="row.status === 1 || row.status === 2 || (row.status === 5 && row.payment_method === 'fubei')"
v-perms="['order.order/split']"
type="primary"
link
@click="openSplitOrder(row)"
>
拆分
</el-button>
<el-button
type="primary"
v-perms="['order.order/wxQrcode']"
link
@click="handleOrderQrcode(row)"
>
小程序码
</el-button>
<el-button
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
v-perms="['order.order/pay']"
type="success"
link
@click="handlePay(row)"
>
支付
</el-button>
<el-button
v-if="row.status === 2"
v-perms="['order.order/refund']"
type="warning"
link
@click="handleRefund(row)"
>
退款
</el-button>
<el-button
v-if="row.status === 1 || (row.status === 5 && row.payment_method === 'fubei')"
v-perms="['order.order/cancel']"
type="danger"
link
@click="handleCancel(row)"
>
取消
</el-button>
<el-button
v-perms="['order.order/delete']"
type="danger"
link
@click="handleDelete(row)"
>
删除
</el-button>
<el-button
v-perms="['order.order/setExempt']"
:type="row.is_exempt === 1 ? 'warning' : 'default'"
link
@click="handleToggleExempt(row)"
>
{{ row.is_exempt === 1 ? '取消豁免' : '设为豁免' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailVisible"
title="订单详情"
width="820px"
>
<el-descriptions :column="2" border v-if="detailData">
<el-descriptions-item label="订单号">
{{ detailData.order_no }}
</el-descriptions-item>
<el-descriptions-item label="订单状态">
<el-tag :type="getStatusTag(detailData.status)">
{{ getStatusText(detailData.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="患者姓名">
{{ detailData.patient?.patient_name || '待关联' }}
</el-descriptions-item>
<el-descriptions-item label="患者电话">
{{ detailData.patient?.phone || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建人">
{{ detailData.creator?.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="收款人userid" v-if="detailData.payee_userid">
{{ detailData.payee_userid }}
</el-descriptions-item>
<el-descriptions-item label="付款人external_userid" v-if="detailData.payer_external_userid">
{{ detailData.payer_external_userid }}
</el-descriptions-item>
<el-descriptions-item label="订单类型">
<el-tag :type="getOrderTypeTag(detailData.order_type)">
{{ getOrderTypeText(detailData.order_type) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="text-red-500 font-semibold">¥{{ detailData.amount }}</span>
</el-descriptions-item>
<el-descriptions-item label="支付方式">
{{ getPaymentMethodText(detailData.payment_method) }}
</el-descriptions-item>
<el-descriptions-item label="支付时间">
{{ detailData.payment_time || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ detailData.create_time }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detailData.remark || '无' }}
</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left" v-if="detailData">操作记录</el-divider>
<p v-if="detailData" class="text-xs text-gray-500 mb-2">谁在什么时间对本单做了哪些操作数据来自服务器记录)。</p>
<el-table
v-if="detailData"
v-loading="actionLogLoading"
:data="actionLogList"
size="small"
max-height="320"
>
<el-table-column label="操作" prop="action_label" min-width="120" show-overflow-tooltip />
<el-table-column label="操作人" prop="admin_name" min-width="100" />
<el-table-column label="操作时间" prop="create_time_text" min-width="160" />
<el-table-column label="说明" prop="summary" min-width="140" show-overflow-tooltip />
</el-table>
<div v-if="detailData" class="flex justify-end mt-3">
<el-pagination
v-model:current-page="actionLogPage"
v-model:page-size="actionLogPageSize"
:total="actionLogTotal"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
small
@size-change="onActionLogSizeChange"
@current-change="loadOrderActionLogs"
/>
</div>
</el-dialog>
<!-- 按人操作统计时间范围内 -->
<el-dialog
v-model="statsDialogVisible"
title="支付单操作统计"
width="520px"
@opened="onStatsDialogOpen"
>
<p v-if="statsRangeText" class="text-xs text-gray-500 mb-3">统计范围{{ statsRangeText }}</p>
<el-form class="mb-3" :inline="true" @submit.prevent>
<el-form-item label="时间范围">
<el-date-picker
v-model="statsDateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始"
end-placeholder="结束"
class="!w-[260px]"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="statsLoading" @click="fetchActionLogStats">查询</el-button>
</el-form-item>
</el-form>
<el-table v-loading="statsLoading" :data="statsList" size="small" max-height="400">
<el-table-column label="操作人" prop="admin_name" min-width="120" show-overflow-tooltip />
<el-table-column label="操作次数" prop="cnt" width="100" align="right" />
</el-table>
</el-dialog>
<el-dialog v-model="assignDialogVisible" title="变更创建人(医助)" width="440px" destroy-on-close>
<p class="text-sm text-gray-600 mb-3">
{{ selectedOrderIds.length }} 条支付单的创建人改为所选医助并写入各单操作日志
</p>
<el-select
v-model="assignForm.assistant_id"
filterable
placeholder="选择医助"
class="w-full"
>
<el-option
v-for="item in assistantOptions"
:key="item.id"
:label="
item.dept_names ? `${item.name}${item.dept_names}` : item.name
"
:value="Number(item.id)"
/>
</el-select>
<template #footer>
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assignLoading" @click="submitBatchAssign">确定</el-button>
</template>
</el-dialog>
<!-- 支付弹窗 -->
<el-dialog
v-model="payDialogVisible"
title="订单支付"
width="500px"
>
<el-form :model="payForm" label-width="100px">
<el-alert
v-if="payForm.fubeiPendingAudit"
type="warning"
:closable="false"
class="mb-3"
title="该单为付呗·待审核,请选择实际到账方式后确认「已支付」"
/>
<el-form-item v-if="!payForm.fubeiPendingAudit" label="支付类型" required>
<el-radio-group v-model="payForm.payType">
<!-- <el-radio label="normal">正常支付</el-radio> -->
<el-radio label="supplement">补单支付</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="payForm.payType === 'normal'">
<el-form-item label="订单号">
<span>{{ payForm.order_no }}</span>
</el-form-item>
<el-form-item label="订单金额">
<span class="text-red-500 font-semibold">¥{{ payForm.amount }}</span>
</el-form-item>
</template>
<template v-if="!payForm.fubeiPendingAudit && payForm.payType === 'supplement'">
<el-form-item label="支付订单号" required>
<el-input
v-model="payForm.supplement_order_no"
placeholder="请输入支付订单号"
/>
</el-form-item>
</template>
<template v-if="payForm.fubeiPendingAudit">
<el-form-item label="订单号">
<span>{{ payForm.order_no }}</span>
</el-form-item>
<el-form-item label="金额">
<span class="text-red-500 font-semibold">¥{{ payForm.amount }}</span>
</el-form-item>
</template>
<el-form-item label="支付方式" required>
<el-radio-group v-model="payForm.payment_method">
<el-radio v-if="payForm.fubeiPendingAudit" label="manual">手动确认到账</el-radio>
<el-radio label="alipay">支付宝</el-radio>
<el-radio label="wechat">微信</el-radio>
<el-radio label="wechat_work">企业微信</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="payDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmPay" :loading="payLoading">确认支付</el-button>
</template>
</el-dialog>
<!-- 创建订单弹窗 -->
<el-dialog
v-model="createDialogVisible"
title="创建订单"
width="600px"
@close="resetCreateForm"
>
<el-form
ref="createFormRef"
:model="createForm"
:rules="createRules"
label-width="100px"
>
<el-form-item label="创建方式">
<el-radio-group v-model="createForm.createType">
<el-radio label="normal">普通订单</el-radio>
<el-radio label="wechat_work">企业微信对外收款</el-radio>
<el-radio label="fubei">付呗</el-radio>
</el-radio-group>
</el-form-item>
<el-alert
v-if="createForm.createType === 'wechat_work'"
type="info"
:closable="false"
show-icon
class="mb-4"
>
<template #title>企业微信对外收款</template>
创建后将生成订单号请在企业微信发起收款时将该订单号填写为商户订单号」,收款成功后系统将自动更新订单状态
</el-alert>
<el-alert
v-if="createForm.createType === 'fubei'"
type="info"
:closable="false"
show-icon
class="mb-4"
>
<template #title>付呗</template>
通过付呗收款的支付单创建后请按实际对账/审核流程在列表中处理
</el-alert>
<el-form-item v-if="createForm.createType === 'fubei'" label="支付单审核">
<el-switch
v-model="createForm.requirePaymentSlipAudit"
active-text="申请审核"
inactive-text="不申请"
/>
<div class="text-xs text-gray-500 mt-1">
开启后新订单的支付状态为待审核」;未开启则为待支付」。
</div>
</el-form-item>
<el-form-item label="患者" prop="patient_id">
<el-select
v-model="createForm.patient_id"
placeholder="请选择患者"
filterable
remote
:remote-method="searchPatients"
:loading="patientLoading"
>
<el-option
v-for="item in patientList"
:key="item.id"
:label="`${item.patient_name} (${item.phone})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="订单类型" prop="order_type">
<el-select v-model="createForm.order_type" placeholder="请选择">
<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-option label="驼奶费用" :value="8" />
</el-select>
</el-form-item>
<el-form-item label="订单金额" prop="amount">
<el-input-number
v-model="createForm.amount"
:min="0"
:step="0.01"
:precision="2"
placeholder="请输入金额"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="createForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注(可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCreateOrder" :loading="createLoading">
{{ createForm.createType === 'wechat_work' ? '创建并获取收款订单号' : '创建订单' }}
</el-button>
</template>
</el-dialog>
<!-- 同步企业微信收款弹窗 -->
<el-dialog
v-model="syncDialogVisible"
title="同步企业微信对外收款"
width="420px"
>
<p class="text-gray-600 mb-4">拉取员工在企业微信内直接发起的对外收款记录自动创建订单未经过后台创建的收款</p>
<el-form label-width="90px">
<el-form-item label="时间范围">
<el-date-picker
v-model="syncDateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:shortcuts="syncDateShortcuts"
/>
</el-form-item>
<el-form-item label="收款人userid">
<el-input v-model="syncPayeeUserid" placeholder="可选,如 GaoXingLiang,排查时若返回 try_payee_userids 可逐个尝试" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="syncDialogVisible = false">取消</el-button>
<el-button type="info" :loading="syncDebugLoading" @click="handleSyncDebug">
排查同步为0
</el-button>
<el-button type="primary" :loading="syncLoading" @click="submitSyncWechatWork">
开始同步
</el-button>
</template>
</el-dialog>
<!-- 编辑订单弹窗 -->
<el-dialog
v-model="editOrderDialogVisible"
title="编辑订单"
width="480px"
>
<el-form v-if="editOrderForm" ref="editOrderFormRef" :model="editOrderForm" :rules="editOrderRules" label-width="100px">
<el-form-item label="关联患者" prop="patient_id">
<el-select
v-model="editOrderForm.patient_id"
filterable
remote
placeholder="搜索患者姓名/手机号,可留空表示待关联"
:remote-method="searchPatientsForEdit"
:loading="editPatientLoading"
clearable
class="w-full"
>
<el-option
v-for="item in editPatientList"
:key="item.id"
:label="`${item.patient_name || '-'} (${item.phone || '-'})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="订单类型" prop="order_type">
<el-select v-model="editOrderForm.order_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-option label="驼奶费用" :value="8" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editOrderDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="editOrderLoading" @click="submitEditOrder">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="splitDialogVisible" title="拆分订单" width="620px" destroy-on-close @closed="splitSource = null">
<template v-if="splitSource">
<p class="text-sm text-gray-600 mb-3">
原订单号 <span class="font-mono">{{ splitSource.order_no }}</span>金额
<span class="text-red-500 font-semibold">¥{{ splitSource.amount }}</span
>每笔子单可分别选择<strong>订单类型</strong>子单继承患者创建人及企微收款信息<strong>已支付</strong>子单继承支付方式支付时间与交易号原单将软删除若已关联业务订单关联会同步到全部子单
</p>
<el-form label-width="88px" @submit.prevent>
<el-form-item label="拆分管数">
<el-input-number
v-model="splitPartCount"
:min="2"
:max="20"
:step="1"
step-strictly
@change="redistributeSplitAmounts"
/>
<span class="text-xs text-gray-400 ml-2">220 </span>
</el-form-item>
</el-form>
<div class="space-y-2 mb-3 max-h-[320px] overflow-y-auto pr-1">
<div v-for="(_amt, idx) in splitAmounts" :key="idx" class="flex items-center gap-2 flex-wrap">
<span class="text-sm text-gray-500 w-14 shrink-0"> {{ idx + 1 }} </span>
<el-select v-model="splitOrderTypes[idx]" placeholder="订单类型" class="w-[136px] shrink-0">
<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-option label="驼奶费用" :value="8" />
</el-select>
<el-input-number
v-model="splitAmounts[idx]"
:min="0.01"
:step="0.01"
:precision="2"
class="flex-1 min-w-[140px]"
controls-position="right"
/>
</div>
</div>
<el-alert
:type="splitSumMatches ? 'success' : 'warning'"
:closable="false"
show-icon
>
<template #title>
子单合计 ¥{{ splitSum.toFixed(2) }}
{{ splitSumMatches ? '与原价一致,可提交' : '须与原价完全一致方可拆分' }}
</template>
</el-alert>
</template>
<template #footer>
<el-button @click="splitDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="splitLoading" :disabled="!splitSumMatches" @click="submitSplitOrder">
确定拆分
</el-button>
</template>
</el-dialog>
<!-- 小程序码弹窗 -->
<el-dialog
v-model="qrcodeDialogVisible"
title="支付小程序码"
width="400px"
:close-on-click-modal="false"
>
<div class="flex flex-col items-center justify-center py-4">
<div v-if="qrcodeLoading" class="text-center">
<el-icon class="is-loading" :size="40">
<Loading />
</el-icon>
<div class="mt-4 text-gray-500">生成中...</div>
</div>
<div v-else-if="qrcodeUrl" class="text-center">
<img :src="qrcodeUrl" alt="小程序二维码" class="w-64 h-64 border border-gray-200 rounded" />
<div class="mt-4 text-sm text-gray-600">
<div>订单号{{ qrcodeOrderNo }}</div>
<div class="mt-2">请使用微信扫描二维码完成支付</div>
</div>
</div>
<div v-else class="text-center text-gray-500">
生成失败请重试
</div>
</div>
<template #footer>
<el-button @click="qrcodeDialogVisible = false">关闭</el-button>
<el-button v-if="!qrcodeLoading" type="primary" @click="handleOrderQrcodeRefresh">
重新生成
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="orderList">
import { computed } from 'vue'
import { usePaging } from '@/hooks/usePaging'
import {
orderLists,
orderDetail,
orderPay,
orderCancel,
orderDelete,
orderRefund,
orderExport,
orderCreate,
orderCreateForWechatWork,
orderEdit,
orderTodayRevenue,
syncWechatWorkBills,
syncWechatWorkBillsDebug,
searchPatients as searchPatientsAPI,
alipayPay,
wechatPay,
orderActionLogs,
orderActionLogStats,
orderBatchAssignAssistant,
orderSplit,
orderSetExempt
} from '@/api/order'
import { getAssistants } from '@/api/tcm'
import { generateOrderQrcode } from '@/api/tcm'
import { Loading } from '@element-plus/icons-vue'
import feedback from '@/utils/feedback'
const queryParams = reactive({
order_no: '',
patient_keyword: '',
assistant_id: undefined as number | undefined,
order_type: '',
status: '',
patient_association: '' as '' | 'pending' | 'associated',
create_time_start: '',
create_time_end: ''
})
const assistantOptions = ref<
Array<{ id: number; name: string; account?: string; dept_names?: string; dept_ids?: number[] }>
>([])
const orderTableRef = ref<any>(null)
const selectedOrderRows = ref<any[]>([])
const selectedOrderIds = computed(() => selectedOrderRows.value.map((r) => r.id as number).filter((id) => id > 0))
const assignDialogVisible = ref(false)
const assignLoading = ref(false)
const assignForm = reactive({
assistant_id: undefined as number | undefined
})
const patientAssociationTab = ref<'' | 'pending' | 'associated'>('')
const todayRevenue = ref({ amount: '0.00', count: 0 })
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: orderLists,
params: queryParams
})
const handleOrderSelectionChange = (rows: any[]) => {
selectedOrderRows.value = rows || []
}
const openAssignDialog = () => {
if (!selectedOrderIds.value.length) {
feedback.msgWarning('请先勾选要指派的支付单')
return
}
assignForm.assistant_id = undefined
assignDialogVisible.value = true
}
const submitBatchAssign = async () => {
if (assignForm.assistant_id == null) {
feedback.msgWarning('请选择医助')
return
}
assignLoading.value = true
try {
await orderBatchAssignAssistant({
order_ids: [...selectedOrderIds.value],
assistant_id: assignForm.assistant_id
})
feedback.msgSuccess('指派成功')
assignDialogVisible.value = false
orderTableRef.value?.clearSelection()
getLists()
fetchTodayRevenue()
} catch (e: any) {
if (e?.msg) feedback.msgError(e.msg)
} finally {
assignLoading.value = false
}
}
// 搜索
const handleSearch = () => {
resetPage()
}
// Tab 切换
const handleTabChange = (name: string | number) => {
queryParams.patient_association = (name as '' | 'pending' | 'associated') || ''
resetPage()
}
// 获取今日收益
const fetchTodayRevenue = async () => {
try {
const res: any = await orderTodayRevenue()
todayRevenue.value = res?.data || res || { amount: '0.00', count: 0 }
} catch (e) {
todayRevenue.value = { amount: '0.00', count: 0 }
}
}
// 重置
const handleReset = () => {
patientAssociationTab.value = ''
resetParams()
}
const detailVisible = ref(false)
const detailData = ref<any>(null)
const currentOrderIdForLog = ref(0)
const actionLogLoading = ref(false)
const actionLogList = ref<any[]>([])
const actionLogTotal = ref(0)
const actionLogPage = ref(1)
const actionLogPageSize = ref(10)
const statsDialogVisible = ref(false)
const statsLoading = ref(false)
const statsList = ref<any[]>([])
const statsDateRange = ref<[string, string] | null>(null)
const statsRangeText = ref('')
const loadOrderActionLogs = async () => {
if (!currentOrderIdForLog.value) {
return
}
actionLogLoading.value = true
try {
const res: any = await orderActionLogs({
order_id: currentOrderIdForLog.value,
page_no: actionLogPage.value,
page_size: actionLogPageSize.value
})
actionLogList.value = res?.lists || []
actionLogTotal.value = res?.count ?? 0
} catch {
actionLogList.value = []
actionLogTotal.value = 0
} finally {
actionLogLoading.value = false
}
}
const onActionLogSizeChange = () => {
actionLogPage.value = 1
loadOrderActionLogs()
}
const onStatsDialogOpen = () => {
statsDateRange.value = null
fetchActionLogStats()
}
const fetchActionLogStats = async () => {
statsLoading.value = true
try {
const params: { start_time?: string; end_time?: string } = {}
if (statsDateRange.value && statsDateRange.value.length === 2) {
params.start_time = statsDateRange.value[0]
params.end_time = statsDateRange.value[1]
}
const res: any = await orderActionLogStats(Object.keys(params).length ? params : undefined)
statsList.value = res?.list || []
if (res?.start_time && res?.end_time) {
statsRangeText.value = `${res.start_time} ${res.end_time}`
} else {
statsRangeText.value = ''
}
} catch {
statsList.value = []
statsRangeText.value = ''
} finally {
statsLoading.value = false
}
}
const payDialogVisible = ref(false)
const payLoading = ref(false)
const payForm = ref({
order_id: 0,
order_no: '',
amount: 0,
/** 付呗+待审核:简化弹窗,确认后标已付 */
fubeiPendingAudit: false,
payment_method: 'alipay' as 'alipay' | 'wechat' | 'wechat_work' | 'manual',
payType: 'supplement' as 'supplement',
supplement_order_no: ''
})
// 创建订单相关
const createDialogVisible = ref(false)
const createFormRef = ref()
const createLoading = ref(false)
const patientLoading = ref(false)
const patientList = ref<any[]>([])
const createForm = reactive({
createType: 'normal' as 'normal' | 'wechat_work' | 'fubei',
/** 仅「付呗」:开启后订单为待审核(5) */
requirePaymentSlipAudit: false,
patient_id: '',
order_type: '',
amount: 0,
remark: ''
})
const createRules = {
patient_id: [{ required: true, message: '请选择患者', trigger: 'change' }],
order_type: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
amount: [{ required: true, message: '请输入订单金额', trigger: 'blur' }]
}
// 编辑订单相关
const editOrderDialogVisible = ref(false)
const editOrderFormRef = ref()
const editOrderLoading = ref(false)
const editPatientLoading = ref(false)
const editPatientList = ref<any[]>([])
const editOrderForm = ref<{ id: number; patient_id: number | null; order_type: number } | null>(null)
const editOrderRules = {
order_type: [{ required: true, message: '请选择订单类型', trigger: 'change' }]
}
// 搜索患者
const searchPatients = async (query: string) => {
if (!query) {
patientList.value = []
return
}
try {
patientLoading.value = true
const res = await searchPatientsAPI({
keyword: query,
page_no: 1,
page_size: 10
})
patientList.value = res?.lists || []
} catch (error) {
console.error('搜索患者失败:', error)
} finally {
patientLoading.value = false
}
}
// 重置创建表单
const resetCreateForm = () => {
createForm.createType = 'normal'
createForm.requirePaymentSlipAudit = false
createForm.patient_id = ''
createForm.order_type = ''
createForm.amount = 0
createForm.remark = ''
createFormRef.value?.clearValidate()
}
// 打开创建订单弹窗
const handleCreate = () => {
resetCreateForm()
createDialogVisible.value = true
}
// 同步企业微信收款
const syncDialogVisible = ref(false)
const syncLoading = ref(false)
const syncDebugLoading = ref(false)
const syncPayeeUserid = ref('')
const syncDateRange = ref<[string, string]>([
new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
new Date().toISOString().slice(0, 10)
])
const syncDateShortcuts = [
{ text: '最近7天', value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 7 * 86400000)
return [start, end]
}},
{ text: '最近30天', value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 30 * 86400000)
return [start, end]
}},
{ text: '今天', value: () => {
const d = new Date()
return [d, d]
}}
]
const handleSyncWechatWork = () => {
syncDateRange.value = [
new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
new Date().toISOString().slice(0, 10)
]
syncDialogVisible.value = true
}
const handleSyncDebug = async () => {
const range = syncDateRange.value
if (!range || range.length !== 2) {
feedback.msgError('请先选择时间范围')
return
}
try {
syncDebugLoading.value = true
const res: any = await syncWechatWorkBillsDebug({
begin_time: range[0],
end_time: range[1],
payee_userid: syncPayeeUserid.value || undefined
})
const d = res?.data || res
let msg = `配置:${d.config_ok ? '✓' : '✗'} | token:${d.access_token} | errcode:${d.api_errcode} | errmsg:${d.api_errmsg || '-'} | 账单数:${d.bill_count}`
if (d.try_payee_userids?.length) {
msg += `。可尝试收款人: ${d.try_payee_userids.join(', ')}`
}
if (d.api_errcode !== 0 && d.api_errcode !== -999) {
msg += '。提示:需使用「对外收款」应用的Secret(管理后台-应用管理-对外收款-API)'
}
feedback.msgSuccess(msg)
console.log('企业微信同步调试:', d)
} catch (e: any) {
feedback.msgError(e?.message || '排查失败')
} finally {
syncDebugLoading.value = false
}
}
const submitSyncWechatWork = async () => {
const range = syncDateRange.value
if (!range || range.length !== 2) {
feedback.msgError('请选择时间范围')
return
}
try {
syncLoading.value = true
const res: any = await syncWechatWorkBills({
begin_time: range[0],
end_time: range[1]
})
const data = res?.data || res
syncDialogVisible.value = false
getLists()
const msg = `同步完成:新增 ${data.created || 0} 笔,更新 ${data.updated || 0} 笔,跳过 ${data.skipped || 0} 笔` +
(data.errors?.length ? `;错误:${data.errors.join('')}` : '')
feedback.msgSuccess(msg)
fetchTodayRevenue()
} catch (error: any) {
feedback.msgError(error?.message || '同步失败')
} finally {
syncLoading.value = false
}
}
// 提交创建订单
const submitCreateOrder = async () => {
try {
await createFormRef.value?.validate()
createLoading.value = true
if (createForm.createType === 'wechat_work') {
const params = {
patient_id: createForm.patient_id,
order_type: createForm.order_type,
amount: createForm.amount,
remark: createForm.remark
}
const res: any = await orderCreateForWechatWork(params)
const orderNo = res?.order_no ?? res?.data?.order_no
createDialogVisible.value = false
getLists()
feedback.msgSuccess(orderNo ? `订单创建成功,请在企业微信收款时使用订单号:${orderNo}` : '订单创建成功')
} else {
const params: Record<string, unknown> = {
patient_id: createForm.patient_id,
order_type: createForm.order_type,
amount: createForm.amount,
remark: createForm.remark
}
if (createForm.createType === 'fubei') {
params.payment_channel = 'fubei'
params.require_payment_slip_audit = createForm.requirePaymentSlipAudit ? 1 : 0
} else {
params.payment_channel = 'normal'
params.require_payment_slip_audit = 0
}
await orderCreate(params)
const tip =
createForm.createType === 'fubei' && createForm.requirePaymentSlipAudit
? '订单已创建,支付状态为「待审核」'
: '订单创建成功'
feedback.msgSuccess(tip)
createDialogVisible.value = false
getLists()
}
} catch (error) {
console.error('创建订单失败:', error)
} finally {
createLoading.value = false
}
}
// 获取订单类型文本
const getOrderTypeText = (type: number) => {
const typeMap: Record<number, string> = {
1: '挂号费',
2: '问诊费',
3: '药品费用',
4: '首付费用',
5: '尾款费用',
6: '其他费用',
7: '全部费用',
8: '驼奶费用',
}
return typeMap[type] || '未知'
}
// 获取订单类型标签类型
const getOrderTypeTag = (type: number): 'success' | 'info' | 'warning' => {
const tagMap: Record<number, 'success' | 'info' | 'warning'> = {
1: 'success',
2: 'info',
3: 'warning'
}
return tagMap[type] || 'info'
}
// 获取订单状态文本
const getStatusText = (status: number) => {
const statusMap: Record<number, string> = {
1: '待支付',
2: '已支付',
3: '已取消',
4: '已退款',
5: '待审核'
}
return statusMap[status] || '未知'
}
// 获取订单状态标签类型
const getStatusTag = (status: number): 'warning' | 'success' | 'info' | 'danger' => {
const tagMap: Record<number, 'warning' | 'success' | 'info' | 'danger'> = {
1: 'warning',
2: 'success',
3: 'info',
4: 'danger',
5: 'warning'
}
return tagMap[status] || 'info'
}
// 获取支付方式文本
const getPaymentMethodText = (method: string | null) => {
if (!method) return '-'
const methodMap: Record<string, string> = {
alipay: '支付宝',
wechat: '微信',
wechat_work: '企业微信',
fubei: '付呗',
manual: '手动确认'
}
return methodMap[method] || '未知'
}
// 编辑订单
const handleEditOrder = (row: any) => {
editOrderForm.value = {
id: row.id,
patient_id: row.patient_id || null,
order_type: row.order_type
}
editPatientList.value = row.patient ? [row.patient] : []
editOrderDialogVisible.value = true
}
const searchPatientsForEdit = async (query: string) => {
if (!query) {
editPatientList.value = []
return
}
try {
editPatientLoading.value = true
const res = await searchPatientsAPI({ keyword: query, page_no: 1, page_size: 10 })
editPatientList.value = res?.lists || []
} catch (e) {
editPatientList.value = []
} finally {
editPatientLoading.value = false
}
}
const submitEditOrder = async () => {
if (!editOrderForm.value) return
try {
await editOrderFormRef.value?.validate()
editOrderLoading.value = true
await orderEdit({
id: editOrderForm.value.id,
patient_id: editOrderForm.value.patient_id ?? 0,
order_type: editOrderForm.value.order_type
})
feedback.msgSuccess('保存成功')
editOrderDialogVisible.value = false
getLists()
fetchTodayRevenue()
} catch (e: any) {
if (e?.message) feedback.msgError(e.message)
} finally {
editOrderLoading.value = false
}
}
/** 拆分订单(待支付 / 已支付 / 付呗待审核) */
const splitDialogVisible = ref(false)
const splitSource = ref<any>(null)
const splitPartCount = ref(2)
const splitAmounts = ref<number[]>([])
const splitOrderTypes = ref<number[]>([])
const splitLoading = ref(false)
const redistributeSplitAmounts = () => {
if (!splitSource.value) return
const total = Number(splitSource.value.amount)
const n = Math.max(2, Math.min(20, Math.floor(Number(splitPartCount.value) || 2)))
splitPartCount.value = n
const cents = Math.round(total * 100)
const base = Math.floor(cents / n)
const rem = cents % n
const next: number[] = []
for (let i = 0; i < n; i++) {
const c = base + (i < rem ? 1 : 0)
next.push(c / 100)
}
const prevTypes = [...splitOrderTypes.value]
const defaultType = Number(splitSource.value.order_type ?? 6)
splitOrderTypes.value = next.map((_, i) => (i < prevTypes.length ? prevTypes[i]! : defaultType))
splitAmounts.value = next
}
const openSplitOrder = (row: any) => {
splitSource.value = row
splitPartCount.value = 2
redistributeSplitAmounts()
splitDialogVisible.value = true
}
const splitSum = computed(() => splitAmounts.value.reduce((s, x) => s + Number(x || 0), 0))
const splitSumMatches = computed(() => {
if (!splitSource.value) return false
const orig = Number(splitSource.value.amount)
return Math.abs(splitSum.value - orig) < 0.0001
})
const submitSplitOrder = async () => {
if (!splitSource.value || !splitSumMatches.value) return
splitLoading.value = true
try {
await orderSplit({
id: splitSource.value.id,
amounts: splitAmounts.value.map((x) => Number(x)),
order_types: splitOrderTypes.value.map((x) => Number(x))
})
feedback.msgSuccess('拆分成功')
splitDialogVisible.value = false
getLists()
fetchTodayRevenue()
} catch (e: any) {
if (e?.msg) feedback.msgError(e.msg)
} finally {
splitLoading.value = false
}
}
// 查看详情
const handleDetail = async (row: any) => {
try {
const res = await orderDetail({ id: row.id })
detailData.value = res
currentOrderIdForLog.value = row.id
actionLogPage.value = 1
detailVisible.value = true
loadOrderActionLogs()
} catch (error) {
console.error('获取详情失败:', error)
}
}
// 支付订单
const handlePay = (row: any) => {
const fubeiPending = row.status === 5 && row.payment_method === 'fubei'
payForm.value = {
order_id: row.id,
order_no: row.order_no,
amount: row.amount,
fubeiPendingAudit: fubeiPending,
payment_method: fubeiPending ? 'manual' : 'alipay',
payType: 'supplement',
supplement_order_no: ''
}
payDialogVisible.value = true
}
// 确认支付
const confirmPay = async () => {
try {
// 付呗待审核:直接标记已支付
if (payForm.value.fubeiPendingAudit) {
payLoading.value = true
await orderPay({
id: payForm.value.order_id,
payment_method: payForm.value.payment_method
})
feedback.msgSuccess('已更新为已支付')
payDialogVisible.value = false
getLists()
fetchTodayRevenue()
return
}
// 验证补单支付的订单号
if (payForm.value.payType === 'supplement' && !payForm.value.supplement_order_no) {
feedback.msgWarning('请输入支付订单号')
return
}
payLoading.value = true
let payParams: any = {}
if (payForm.value.payType === 'normal') {
// 正常支付
payParams = {
id: payForm.value.order_id,
order_no: payForm.value.order_no,
amount: payForm.value.amount,
payment_method: payForm.value.payment_method
}
} else {
// 补单支付:直接填订单号,不需要校验
payParams = {
order_no: payForm.value.supplement_order_no,
payment_method: payForm.value.payment_method,
is_supplement: 1
}
}
// 企业微信补单:直接调用 orderPay 更新订单状态
if (payForm.value.payment_method === 'wechat_work') {
await orderPay(payParams)
feedback.msgSuccess('补单成功')
} else {
// 调用对应的支付网关
let payResult: any
if (payForm.value.payment_method === 'alipay') {
payResult = await alipayPay(payParams)
} else if (payForm.value.payment_method === 'wechat') {
payResult = await wechatPay(payParams)
}
// 支付网关返回支付链接或二维码
if (payResult && payResult.pay_url) {
// 如果是支付宝,跳转到支付页面
if (payForm.value.payment_method === 'alipay') {
window.location.href = payResult.pay_url
} else if (payForm.value.payment_method === 'wechat') {
// 如果是微信,显示二维码或其他处理
feedback.msgSuccess('请扫描二维码进行支付')
}
}
}
payDialogVisible.value = false
// 延迟刷新列表,等待支付完成
setTimeout(() => {
getLists()
}, 2000)
} catch (error) {
console.error('支付失败:', error)
feedback.msgError('支付失败,请重试')
} finally {
payLoading.value = false
}
}
// 取消订单
const handleCancel = async (row: any) => {
try {
await feedback.confirm('确定要取消该订单吗?')
await orderCancel({ id: row.id })
feedback.msgSuccess('取消成功')
getLists()
} catch (error) {
// 用户取消操作
}
}
// 退款订单
const handleRefund = async (row: any) => {
try {
await feedback.confirm('确定要退款该订单吗?')
await orderRefund({ id: row.id })
feedback.msgSuccess('退款成功')
getLists()
} catch (error) {
// 用户取消操作
}
}
// 删除订单
const handleDelete = async (row: any) => {
try {
await feedback.confirm('确定要删除该订单吗?此操作不可恢复')
await orderDelete({ id: row.id })
feedback.msgSuccess('删除成功')
getLists()
} catch (error) {
// 用户取消操作
}
}
const handleToggleExempt = async (row: any) => {
const newVal: 0 | 1 = row.is_exempt === 1 ? 0 : 1
const actionText = newVal === 1 ? '设为豁免' : '取消豁免'
try {
await feedback.confirm(`确定要${actionText}该支付单吗?豁免后关联此单的业务订单将跳过定金门槛校验。`)
await orderSetExempt({ id: row.id, is_exempt: newVal })
feedback.msgSuccess(`${actionText}成功`)
getLists()
} catch (error) {
// 用户取消操作
}
}
// 小程序码相关
const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
const qrcodeUrl = ref('')
const qrcodeOrderNo = ref('')
const handleOrderQrcode = async (row: any) => {
if (!row.order_no) {
feedback.msgWarning('订单号不存在')
return
}
qrcodeOrderNo.value = row.order_no
qrcodeDialogVisible.value = true
qrcodeLoading.value = true
qrcodeUrl.value = ''
try {
const res = await generateOrderQrcode({ order_no: row.order_no })
if (res?.qrcode_url) {
qrcodeUrl.value = res.qrcode_url
} else {
feedback.msgError('二维码生成失败')
}
} catch (e: any) {
feedback.msgError(e?.msg || '生成二维码失败')
} finally {
qrcodeLoading.value = false
}
}
const handleOrderQrcodeRefresh = () => {
if (qrcodeOrderNo.value) {
handleOrderQrcode({ order_no: qrcodeOrderNo.value })
}
}
const loadAssistantOptions = async () => {
try {
const list = await getAssistants()
assistantOptions.value = Array.isArray(list) ? list : []
} catch {
assistantOptions.value = []
}
}
onMounted(() => {
loadAssistantOptions()
getLists()
fetchTodayRevenue()
})
</script>
<style scoped lang="scss">
.order-list {
padding: 20px;
}
</style>