Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb51e12d1 | ||
|
|
4f9494db0b | ||
|
|
4e87b080a0 | ||
|
|
2f3038aba1 | ||
|
|
131fb7d9c7 | ||
|
|
762a724204 | ||
|
|
8090e0aab3 | ||
|
|
9df6330d41 | ||
|
|
fd63e23eaf | ||
|
|
c1143f4629 | ||
|
|
149fdffd49 | ||
|
|
439d5a3ec9 | ||
|
|
9e24d49210 | ||
|
|
90ac43fc82 | ||
|
|
8b6b709987 | ||
|
|
fb6ca9ce85 | ||
|
|
08290b329e | ||
|
|
1d66f84bd3 | ||
|
|
a2d9c1a03f | ||
|
|
d79db88349 | ||
|
|
9f5fb650af | ||
|
|
15b4339c90 | ||
|
|
5ce331394d | ||
|
|
7ba9dabdb4 | ||
|
|
611f3dcd5c | ||
|
|
d3388b160e | ||
|
|
f83d4c06cb | ||
|
|
4941ea3e21 | ||
|
|
e35696e153 | ||
|
|
800c6af7f1 | ||
|
|
e1df584ed5 | ||
|
|
876ef0f8a0 | ||
|
|
928f75e016 | ||
|
|
37fa160c24 | ||
|
|
0eaacbb4ce | ||
|
|
1e8b5c4646 | ||
|
|
fdcfa30810 | ||
|
|
fabaa84373 | ||
|
|
da017bdf20 | ||
|
|
5d94ffab3e | ||
|
|
796ca12fb8 | ||
|
|
4caa98af3a | ||
|
|
c644f2554f | ||
|
|
bb87e4fb4c | ||
|
|
e83dac756d | ||
|
|
0c1709d589 | ||
|
|
a3892001d3 | ||
|
|
039105580c | ||
|
|
f47f431bdf | ||
|
|
7add364e20 | ||
|
|
0ebea74fde | ||
|
|
1156787327 | ||
|
|
15cf6324f6 | ||
|
|
1338b8735c | ||
|
|
82f229adf2 | ||
|
|
048e43a662 | ||
|
|
d8d4da197e | ||
|
|
a0325a8fb1 | ||
|
|
091db50841 | ||
|
|
1774a87dd7 | ||
|
|
62d15efbaf | ||
|
|
c468c57cda | ||
|
|
bacdd6386f | ||
|
|
ea290c4c7b | ||
|
|
31b4afdcb7 | ||
|
|
379d2ef404 | ||
|
|
5cdb7ebb9e | ||
|
|
496a2b870b | ||
|
|
471bdf12b7 | ||
|
|
8fe4683e64 | ||
|
|
c34409edde | ||
|
|
0a37335f3c |
@@ -30,3 +30,6 @@ bin-release/
|
||||
/.claude
|
||||
/.claude
|
||||
/.kbctx
|
||||
/server/.spool
|
||||
/server/.claude
|
||||
/.spool
|
||||
|
||||
@@ -66,6 +66,16 @@ export function appointmentLists(params: any) {
|
||||
return request.get({ url: '/doctor.appointment/lists', params })
|
||||
}
|
||||
|
||||
/** 后台编辑挂号(预约日期/时段/类型/状态/备注/医助) */
|
||||
export function appointmentAdminEdit(params: any) {
|
||||
return request.post({ url: '/doctor.appointment/edit', params })
|
||||
}
|
||||
|
||||
/** 批量修改挂号渠道来源(与 edit 共用 doctor.appointment/edit 权限) */
|
||||
export function appointmentBatchEditChannel(params: any) {
|
||||
return request.post({ url: '/doctor.appointment/batchEditChannel', params })
|
||||
}
|
||||
|
||||
// 获取挂号详情
|
||||
export function appointmentDetail(params: any) {
|
||||
return request.get({ url: '/doctor.appointment/detail', params })
|
||||
|
||||
@@ -104,3 +104,8 @@ export function orderActionLogStats(params?: { start_time?: string; end_time?: s
|
||||
export function orderBatchAssignAssistant(params: { order_ids: number[]; assistant_id: number }) {
|
||||
return request.post({ url: '/order.order/assignAssistant', params })
|
||||
}
|
||||
|
||||
/** 设置/取消支付单豁免权 */
|
||||
export function orderSetExempt(params: { id: number; is_exempt: 0 | 1 }) {
|
||||
return request.post({ url: '/order.order/setExempt', params })
|
||||
}
|
||||
|
||||
@@ -103,3 +103,79 @@ export function doctorDailyStatsOverview(params: {
|
||||
}) {
|
||||
return request.get({ url: '/stats.doctorDailyStats/overview', params })
|
||||
}
|
||||
|
||||
/** 提成结算接口计算较重,单独放宽超时(默认多为 60s) */
|
||||
const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
|
||||
|
||||
/** 提成结算业绩(独立于业绩看板 yejiStats) */
|
||||
export function commissionSettlementOverview(params: {
|
||||
settlement_month: string
|
||||
/** 与 tcm.prescriptionOrder/lists 同源:create_time between */
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
/** 默认 3=履约完成,与列表 fulfillment_status 一致 */
|
||||
fulfillment_status?: number
|
||||
/** 传 1 时仅统计 is_system_auto=1;显式时段下默认不传(含手动) */
|
||||
require_system_auto_prescription?: 0 | 1
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
}) {
|
||||
return request.get(
|
||||
{ url: '/stats.commissionSettlement/overview', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function commissionSettlementDeptOptions() {
|
||||
return request.get({ url: '/stats.commissionSettlement/deptOptions' })
|
||||
}
|
||||
|
||||
export function commissionSettlementChannelOptions() {
|
||||
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
|
||||
}
|
||||
|
||||
/** 提成核对:订单明细分页 bucket: 空|current|deferred;appt_channel_value 可与 assistant_id/doctor_id 组合(0=未匹配挂号渠道) */
|
||||
export function commissionSettlementOrderLines(params: {
|
||||
settlement_month: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
fulfillment_status?: number
|
||||
require_system_auto_prescription?: 0 | 1
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
bucket?: string
|
||||
assistant_id?: number
|
||||
doctor_id?: number
|
||||
appt_channel_value?: number
|
||||
}) {
|
||||
return request.get(
|
||||
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function commissionSettlementConfirmStatus(params: {
|
||||
settlement_month: string
|
||||
dept_ids?: number[] | string
|
||||
channel_code?: string
|
||||
}) {
|
||||
return request.get({ url: '/stats.commissionSettlement/confirmStatus', params })
|
||||
}
|
||||
|
||||
export function commissionSettlementSaveReconcile(params: Record<string, any>) {
|
||||
return request.post({ url: '/stats.commissionSettlement/saveReconcile', params })
|
||||
}
|
||||
|
||||
export function commissionSettlementConfirmFinalize(params: Record<string, any>) {
|
||||
return request.post(
|
||||
{ url: '/stats.commissionSettlement/confirmFinalize', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
/** 撤回「确定本期业绩」:清除顺延结转,状态变为可再次核对/确定 */
|
||||
export function commissionSettlementConfirmRevoke(params: Record<string, any>) {
|
||||
return request.post({ url: '/stats.commissionSettlement/confirmRevoke', params })
|
||||
}
|
||||
|
||||
@@ -319,6 +319,16 @@ export function prescriptionEdit(params: any) {
|
||||
return request.post({ url: '/tcm.prescription/edit', params })
|
||||
}
|
||||
|
||||
/** 仅修正处方笺患者姓名、手机号与性别(不改审核状态),写入业务订单日志表 */
|
||||
export function prescriptionPatchPatient(params: {
|
||||
id: number
|
||||
patient_name: string
|
||||
phone: string
|
||||
gender: number
|
||||
}) {
|
||||
return request.post({ url: '/tcm.prescription/patchPatient', params })
|
||||
}
|
||||
|
||||
// 删除处方
|
||||
export function prescriptionDelete(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescription/delete', params })
|
||||
@@ -363,6 +373,11 @@ export function prescriptionOrderLists(params: any) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/lists', params })
|
||||
}
|
||||
|
||||
/** 处方业务订单导出(export=1 预估条数,export=2 下载 Excel) */
|
||||
export function prescriptionOrderExport(params: any) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/export', params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊单下可关联的支付单(已支付 zyt_order;已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选)。
|
||||
* 服务端仅返回创建时间在 2026-04-20(含)之后的支付单;编辑时本单已关联的旧单仍会出现在列表中。
|
||||
@@ -485,6 +500,11 @@ export function prescriptionOrderUpdateAmount(params: { id: number; amount: numb
|
||||
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
|
||||
}
|
||||
|
||||
/** 设置发货类型:gancao 甘草药房 / direct 洛阳药房 */
|
||||
export function prescriptionOrderSetShipMode(params: { id: number; ship_mode: 'gancao' | 'direct' }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/setShipMode', params })
|
||||
}
|
||||
|
||||
// ========== 处方库 ==========
|
||||
|
||||
// 处方库列表
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<el-button>导出</el-button>
|
||||
</template>
|
||||
<div>
|
||||
<p v-if="props.exportHint" class="text-sm text-gray-500 mb-3 leading-relaxed">{{ props.exportHint }}</p>
|
||||
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="数据量:">
|
||||
预计导出{{ exportData.count }}条数据, 共{{ exportData.sum_page }}页,每页{{
|
||||
@@ -79,6 +80,11 @@ const props = defineProps({
|
||||
fetchFun: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
/** 可选:导出弹窗内提示文案(如说明与列表筛选一致) */
|
||||
exportHint: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const popupRef = shallowRef<InstanceType<typeof Popup>>()
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
<template>
|
||||
<div class="guahao-list">
|
||||
<el-card class="!border-none" shadow="never">
|
||||
<el-form class="mb-[-16px]" :model="queryParams" :inline="true" @submit.prevent>
|
||||
<el-form-item label="预约日期">
|
||||
<daterange-picker
|
||||
v-model:startTime="queryParams.start_date"
|
||||
v-model:endTime="queryParams.end_date"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="患者姓名">
|
||||
<el-input
|
||||
v-model="queryParams.patient_name"
|
||||
placeholder="模糊搜索"
|
||||
clearable
|
||||
class="!w-[160px]"
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="医生">
|
||||
<el-input
|
||||
v-model="queryParams.doctor_name"
|
||||
placeholder="模糊搜索"
|
||||
clearable
|
||||
class="!w-[140px]"
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="医助">
|
||||
<el-select
|
||||
v-model="queryParams.assistant_id"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-[180px]"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in assistantOptions"
|
||||
:key="a.id"
|
||||
:label="a.name + (a.account ? ` (${a.account})` : '')"
|
||||
:value="a.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-[130px]">
|
||||
<el-option label="已预约" :value="1" />
|
||||
<el-option label="已取消" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已过号" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道">
|
||||
<el-select
|
||||
v-model="queryParams.channel_source"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-[200px]"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="String(item.value)"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="!border-none mt-4" shadow="never">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-button
|
||||
v-perms="['doctor.appointment/edit']"
|
||||
type="primary"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="openBatchChannelDialog"
|
||||
>
|
||||
批量修改渠道
|
||||
</el-button>
|
||||
<span v-if="selectedRows.length > 0" class="text-sm text-gray-500">
|
||||
已选 {{ selectedRows.length }} 条
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="pager.loading"
|
||||
row-key="id"
|
||||
:data="pager.lists"
|
||||
size="large"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" reserve-selection />
|
||||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||||
<el-table-column label="诊单/患者" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<div class="text-sm">
|
||||
<div class="font-medium">{{ row.patient_name || '—' }}</div>
|
||||
<div class="text-gray-500">{{ maskPhone(row.patient_phone) }}</div>
|
||||
<div class="text-xs text-gray-400">诊单 #{{ row.diagnosis_id ?? row.patient_id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="医生" prop="doctor_name" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="医助" prop="assistant_name" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="预约时间" min-width="128">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.appointment_date }}</div>
|
||||
<div class="text-gray-500">{{ formatHm(row.appointment_time) }} · {{ row.period_desc }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" prop="appointment_type_desc" width="100" />
|
||||
<el-table-column label="渠道" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div class="text-sm">{{ row.channel_source_desc || '—' }}</div>
|
||||
<div v-if="row.channel_source_detail" class="text-xs text-gray-400 truncate">
|
||||
{{ row.channel_source_detail }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="96" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="
|
||||
row.status === 1 ? 'success' : row.status === 2 ? 'info' : row.status === 3 ? 'primary' : 'danger'
|
||||
"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ row.status_desc }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-perms="['doctor.appointment/edit']" type="primary" link @click="openEdit(row)">
|
||||
编辑
|
||||
</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="editVisible" title="编辑挂号" width="560px" destroy-on-close @closed="resetEditForm">
|
||||
<el-form :model="editForm" label-width="96px">
|
||||
<el-form-item label="预约日期" required>
|
||||
<el-date-picker
|
||||
v-model="editForm.appointment_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预约时间" required>
|
||||
<el-time-picker
|
||||
v-model="editForm.appointment_time"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
placeholder="时间"
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时段" required>
|
||||
<el-radio-group v-model="editForm.period">
|
||||
<el-radio-button label="morning">上午</el-radio-button>
|
||||
<el-radio-button label="afternoon">下午</el-radio-button>
|
||||
<el-radio-button label="all">全天</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="问诊类型" required>
|
||||
<el-select v-model="editForm.appointment_type" class="!w-full">
|
||||
<el-option label="视频问诊" value="video" />
|
||||
<el-option label="图文问诊" value="text" />
|
||||
<el-option label="电话问诊" value="phone" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道来源" required>
|
||||
<el-select
|
||||
v-model="editForm.channel_source"
|
||||
placeholder="请选择渠道来源"
|
||||
filterable
|
||||
class="!w-full"
|
||||
@change="onChannelSourceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="String(item.value)"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="needsChannelSourceDetail" label="自媒体补充" required>
|
||||
<el-input
|
||||
v-model="editForm.channel_source_detail"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入自媒体相关补充内容"
|
||||
clearable
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" required>
|
||||
<el-select v-model="editForm.status" class="!w-full">
|
||||
<el-option label="已预约" :value="1" />
|
||||
<el-option label="已取消" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已过号" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="挂号医助">
|
||||
<el-select
|
||||
v-model="editForm.assistant_id"
|
||||
placeholder="不修改可留空"
|
||||
clearable
|
||||
filterable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in assistantOptions"
|
||||
:key="a.id"
|
||||
:label="a.name + (a.account ? ` (${a.account})` : '')"
|
||||
:value="a.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="batchChannelVisible"
|
||||
title="批量修改渠道"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
@closed="resetBatchChannelForm"
|
||||
>
|
||||
<p class="text-sm text-gray-600 mb-4">将对已选 {{ selectedRows.length }} 条挂号写入同一渠道来源(仅改渠道,不改其它字段)。</p>
|
||||
<el-form :model="batchChannelForm" label-width="96px">
|
||||
<el-form-item label="渠道来源" required>
|
||||
<el-select
|
||||
v-model="batchChannelForm.channel_source"
|
||||
placeholder="请选择渠道来源"
|
||||
filterable
|
||||
class="!w-full"
|
||||
@change="onBatchChannelSourceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="String(item.value)"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="needsBatchChannelSourceDetail" label="自媒体补充" required>
|
||||
<el-input
|
||||
v-model="batchChannelForm.channel_source_detail"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入自媒体相关补充内容"
|
||||
clearable
|
||||
class="!w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchChannelVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchChannelSaving" @click="submitBatchChannel">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="consumerPrescriptionGuahao">
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { appointmentAdminEdit, appointmentBatchEditChannel, appointmentLists } from '@/api/doctor'
|
||||
import { getAssistants } from '@/api/tcm'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const queryParams = reactive({
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
patient_name: '',
|
||||
doctor_name: '',
|
||||
assistant_id: undefined as number | undefined,
|
||||
status: '' as number | '',
|
||||
channel_source: '' as string
|
||||
})
|
||||
|
||||
const queryInit = {
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
patient_name: '',
|
||||
doctor_name: '',
|
||||
assistant_id: undefined as number | undefined,
|
||||
status: '' as number | '',
|
||||
channel_source: ''
|
||||
}
|
||||
|
||||
const { pager, getLists, resetPage } = usePaging({
|
||||
fetchFun: appointmentLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
const assistantOptions = ref<Array<{ id: number; name: string; account?: string }>>([])
|
||||
|
||||
const tableRef = ref<{ clearSelection?: () => void } | null>(null)
|
||||
const selectedRows = ref<Record<string, unknown>[]>([])
|
||||
|
||||
function onSelectionChange(rows: Record<string, unknown>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
/** 与诊单预约弹窗一致:这些字典 name 需填「自媒体补充」 */
|
||||
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
||||
'自媒体4H',
|
||||
'自媒体3Q',
|
||||
'自媒体3H',
|
||||
'自媒体2H',
|
||||
'自媒体2Q'
|
||||
])
|
||||
|
||||
function channelNameRequiresSelfMediaDetail(name: string) {
|
||||
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
||||
}
|
||||
|
||||
const channelOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
|
||||
const selectedChannelDictName = computed(() => {
|
||||
const v = editForm.channel_source
|
||||
if (v === '' || v == null) return ''
|
||||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(v))
|
||||
return row ? String(row.name ?? '').trim() : ''
|
||||
})
|
||||
|
||||
const needsChannelSourceDetail = computed(() => channelNameRequiresSelfMediaDetail(selectedChannelDictName.value))
|
||||
|
||||
const batchSelectedChannelDictName = computed(() => {
|
||||
const v = batchChannelForm.channel_source
|
||||
if (v === '' || v == null) return ''
|
||||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(v))
|
||||
return row ? String(row.name ?? '').trim() : ''
|
||||
})
|
||||
|
||||
const needsBatchChannelSourceDetail = computed(() =>
|
||||
channelNameRequiresSelfMediaDetail(batchSelectedChannelDictName.value)
|
||||
)
|
||||
|
||||
function onBatchChannelSourceChange(val: string) {
|
||||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(val))
|
||||
const name = row ? String(row.name ?? '').trim() : ''
|
||||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||||
batchChannelForm.channel_source_detail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const batchChannelVisible = ref(false)
|
||||
const batchChannelSaving = ref(false)
|
||||
const batchChannelForm = reactive({
|
||||
channel_source: '' as string,
|
||||
channel_source_detail: '' as string
|
||||
})
|
||||
|
||||
function openBatchChannelDialog() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
feedback.msgWarning('请先勾选挂号记录')
|
||||
return
|
||||
}
|
||||
batchChannelForm.channel_source = ''
|
||||
batchChannelForm.channel_source_detail = ''
|
||||
batchChannelVisible.value = true
|
||||
}
|
||||
|
||||
function resetBatchChannelForm() {
|
||||
batchChannelForm.channel_source = ''
|
||||
batchChannelForm.channel_source_detail = ''
|
||||
}
|
||||
|
||||
async function submitBatchChannel() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
feedback.msgWarning('请先勾选挂号记录')
|
||||
return
|
||||
}
|
||||
if (!batchChannelForm.channel_source) {
|
||||
feedback.msgError('请选择渠道来源')
|
||||
return
|
||||
}
|
||||
if (needsBatchChannelSourceDetail.value && !batchChannelForm.channel_source_detail.trim()) {
|
||||
feedback.msgError('请填写自媒体补充说明')
|
||||
return
|
||||
}
|
||||
batchChannelSaving.value = true
|
||||
try {
|
||||
const ids = selectedRows.value.map((r) => Number(r.id))
|
||||
await appointmentBatchEditChannel({
|
||||
ids,
|
||||
channel_source: batchChannelForm.channel_source,
|
||||
channel_source_detail: batchChannelForm.channel_source_detail.trim()
|
||||
})
|
||||
feedback.msgSuccess('批量修改成功')
|
||||
batchChannelVisible.value = false
|
||||
tableRef.value?.clearSelection?.()
|
||||
selectedRows.value = []
|
||||
getLists()
|
||||
} finally {
|
||||
batchChannelSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onChannelSourceChange(val: string) {
|
||||
const row = channelOptions.value.find((item: { value: string }) => String(item.value) === String(val))
|
||||
const name = row ? String(row.name ?? '').trim() : ''
|
||||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||||
editForm.channel_source_detail = ''
|
||||
}
|
||||
}
|
||||
|
||||
const editVisible = ref(false)
|
||||
const editSaving = ref(false)
|
||||
const editForm = reactive({
|
||||
id: 0,
|
||||
appointment_date: '',
|
||||
appointment_time: '' as string,
|
||||
period: 'morning' as 'morning' | 'afternoon' | 'all',
|
||||
appointment_type: 'video',
|
||||
status: 1,
|
||||
remark: '',
|
||||
assistant_id: undefined as number | undefined,
|
||||
channel_source: '' as string,
|
||||
channel_source_detail: '' as string
|
||||
})
|
||||
|
||||
function maskPhone(phone?: string) {
|
||||
const p = String(phone || '')
|
||||
if (p.length >= 11) {
|
||||
return p.slice(0, 3) + '****' + p.slice(-4)
|
||||
}
|
||||
return p || '—'
|
||||
}
|
||||
|
||||
function formatHm(t?: string) {
|
||||
const s = String(t || '')
|
||||
return s.length >= 5 ? s.slice(0, 5) : s || '—'
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
Object.assign(queryParams, queryInit)
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function rowPeriod(row: Record<string, unknown>): 'morning' | 'afternoon' | 'all' {
|
||||
const v = String(row.period ?? row.type ?? 'morning')
|
||||
if (v === 'afternoon' || v === 'all') {
|
||||
return v
|
||||
}
|
||||
return 'morning'
|
||||
}
|
||||
|
||||
async function loadAssistants() {
|
||||
try {
|
||||
const res: any = await getAssistants()
|
||||
assistantOptions.value = res?.lists ?? res ?? []
|
||||
} catch {
|
||||
assistantOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannelOptions() {
|
||||
try {
|
||||
const data: any = await getDictData({ type: 'channels' })
|
||||
const rows = (data?.channels || []).filter((row: { status?: number }) => row.status !== 0)
|
||||
rows.sort((a: { sort?: number; id?: number }, b: { sort?: number; id?: number }) => {
|
||||
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||||
if (ds !== 0) return ds
|
||||
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||||
})
|
||||
channelOptions.value = rows
|
||||
} catch {
|
||||
channelOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(row: Record<string, unknown>) {
|
||||
editForm.id = Number(row.id)
|
||||
editForm.appointment_date = String(row.appointment_date || '')
|
||||
editForm.appointment_time = formatHm(String(row.appointment_time || ''))
|
||||
editForm.period = rowPeriod(row)
|
||||
editForm.appointment_type = String(row.appointment_type || 'video')
|
||||
editForm.status = Number(row.status)
|
||||
editForm.remark = String(row.remark || '')
|
||||
const raw = row.appointment_assistant_id
|
||||
editForm.assistant_id =
|
||||
raw !== undefined && raw !== null && raw !== '' ? Number(raw) : undefined
|
||||
editForm.channel_source = String(row.channel_source ?? row.channels ?? '')
|
||||
editForm.channel_source_detail = String(row.channel_source_detail ?? '')
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.id = 0
|
||||
editForm.appointment_date = ''
|
||||
editForm.appointment_time = ''
|
||||
editForm.period = 'morning'
|
||||
editForm.appointment_type = 'video'
|
||||
editForm.status = 1
|
||||
editForm.remark = ''
|
||||
editForm.assistant_id = undefined
|
||||
editForm.channel_source = ''
|
||||
editForm.channel_source_detail = ''
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editForm.appointment_date) {
|
||||
feedback.msgError('请选择预约日期')
|
||||
return
|
||||
}
|
||||
if (!editForm.appointment_time) {
|
||||
feedback.msgError('请选择预约时间')
|
||||
return
|
||||
}
|
||||
if (!editForm.channel_source) {
|
||||
feedback.msgError('请选择渠道来源')
|
||||
return
|
||||
}
|
||||
if (needsChannelSourceDetail.value && !editForm.channel_source_detail.trim()) {
|
||||
feedback.msgError('请填写自媒体补充说明')
|
||||
return
|
||||
}
|
||||
editSaving.value = true
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
id: editForm.id,
|
||||
appointment_date: editForm.appointment_date,
|
||||
appointment_time: editForm.appointment_time,
|
||||
period: editForm.period,
|
||||
appointment_type: editForm.appointment_type,
|
||||
status: editForm.status,
|
||||
remark: editForm.remark,
|
||||
assistant_id: editForm.assistant_id ?? '',
|
||||
channel_source: editForm.channel_source,
|
||||
channel_source_detail: editForm.channel_source_detail.trim()
|
||||
}
|
||||
await appointmentAdminEdit(payload)
|
||||
feedback.msgSuccess('保存成功')
|
||||
editVisible.value = false
|
||||
getLists()
|
||||
} finally {
|
||||
editSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAssistants()
|
||||
loadChannelOptions()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
getLists()
|
||||
})
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
@@ -216,6 +216,15 @@
|
||||
<el-button type="primary" link @click="handleView(row)" v-perms="['cf.prescription/read']">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="Number(row.void_status) !== 1"
|
||||
type="primary"
|
||||
link
|
||||
v-perms="['tcm.prescription/patchPatient']"
|
||||
@click="openPatchPatientDialog(row)"
|
||||
>
|
||||
改姓名手机
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!Number(row.has_prescription_order) && Number(row.void_status) !== 1"
|
||||
type="success"
|
||||
@@ -548,12 +557,20 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="患者姓名" prop="patient_name">
|
||||
<el-input v-model="editForm.patient_name" placeholder="请输入患者姓名" />
|
||||
<el-input
|
||||
v-model="editForm.patient_name"
|
||||
placeholder="请输入患者姓名"
|
||||
:disabled="editMode === 'edit'"
|
||||
/>
|
||||
</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-input
|
||||
v-model="editForm.visit_no"
|
||||
placeholder="自动生成或手动输入"
|
||||
:disabled="editMode === 'edit'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -972,6 +989,48 @@
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 修正处方笺姓名 / 手机号(zyt_tcm_prescription,写入订单日志) -->
|
||||
<el-dialog
|
||||
v-model="patchPatientVisible"
|
||||
title="修正姓名、性别与手机号"
|
||||
width="440px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="resetPatchPatientForm"
|
||||
>
|
||||
<el-form
|
||||
ref="patchPatientFormRef"
|
||||
:model="patchPatientForm"
|
||||
:rules="patchPatientRules"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="处方编号">
|
||||
<span class="text-gray-700">#{{ patchPatientForm.id || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="患者姓名" prop="patient_name">
|
||||
<el-input v-model="patchPatientForm.patient_name" maxlength="50" show-word-limit placeholder="处方笺展示姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-radio-group v-model="patchPatientForm.gender">
|
||||
<el-radio :label="1">男</el-radio>
|
||||
<el-radio :label="0">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="patchPatientForm.phone" maxlength="20" placeholder="处方笺展示手机号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p class="text-xs text-gray-500 -mt-2 mb-2">
|
||||
仅更新处方表 patient_name、gender、phone,不改变审核状态;记录写入业务订单操作日志(有关联订单时可在此订单日志中查看)。
|
||||
</p>
|
||||
<template #footer>
|
||||
<el-button @click="patchPatientVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="patchPatientSubmitLoading" @click="submitPatchPatient">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 从消费者处方创建业务订单(zyt_tcm_prescription_order,与支付单 zyt_order 分离) -->
|
||||
<el-dialog
|
||||
v-model="createOrderVisible"
|
||||
@@ -1436,6 +1495,7 @@ import {
|
||||
prescriptionLists,
|
||||
prescriptionAdd,
|
||||
prescriptionEdit,
|
||||
prescriptionPatchPatient,
|
||||
prescriptionDelete,
|
||||
prescriptionAudit,
|
||||
prescriptionDetail,
|
||||
@@ -1523,6 +1583,66 @@ const auditTargetId = ref(0)
|
||||
const auditRemark = ref('')
|
||||
const auditLoading = ref(false)
|
||||
|
||||
/** 修正处方笺显示用姓名 / 手机号 */
|
||||
const patchPatientVisible = ref(false)
|
||||
const patchPatientSubmitLoading = ref(false)
|
||||
const patchPatientFormRef = ref<FormInstance>()
|
||||
const patchPatientForm = reactive({
|
||||
id: 0,
|
||||
patient_name: '',
|
||||
gender: 1 as 0 | 1,
|
||||
phone: ''
|
||||
})
|
||||
const patchPatientRules: FormRules = {
|
||||
patient_name: [{ required: true, message: '请输入患者姓名', trigger: 'blur' }],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function openPatchPatientDialog(row: Record<string, unknown>) {
|
||||
patchPatientForm.id = Number(row.id) || 0
|
||||
patchPatientForm.patient_name = String(row.patient_name ?? '').trim()
|
||||
const g = Number(row.gender)
|
||||
patchPatientForm.gender = g === 0 || g === 1 ? (g as 0 | 1) : 1
|
||||
patchPatientForm.phone = String(row.phone ?? '').trim()
|
||||
patchPatientVisible.value = true
|
||||
}
|
||||
|
||||
function resetPatchPatientForm() {
|
||||
patchPatientForm.id = 0
|
||||
patchPatientForm.patient_name = ''
|
||||
patchPatientForm.gender = 1
|
||||
patchPatientForm.phone = ''
|
||||
patchPatientFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function submitPatchPatient() {
|
||||
const form = patchPatientFormRef.value
|
||||
if (!form) return
|
||||
try {
|
||||
await form.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
patchPatientSubmitLoading.value = true
|
||||
try {
|
||||
await prescriptionPatchPatient({
|
||||
id: patchPatientForm.id,
|
||||
patient_name: patchPatientForm.patient_name.trim(),
|
||||
phone: patchPatientForm.phone.trim(),
|
||||
gender: patchPatientForm.gender
|
||||
})
|
||||
feedback.msgSuccess('已保存')
|
||||
patchPatientVisible.value = false
|
||||
getLists()
|
||||
} catch (e: unknown) {
|
||||
const msg = e && typeof e === 'object' && 'msg' in e ? String((e as { msg?: string }).msg) : ''
|
||||
feedback.msgError(msg || '保存失败')
|
||||
} finally {
|
||||
patchPatientSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 从消费者处方创建订单 */
|
||||
const createOrderVisible = ref(false)
|
||||
/** 创建订单分步向导:0 患者与收货 / 1 服务与支付单 / 2 金额与确认 */
|
||||
|
||||
@@ -263,9 +263,22 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="w-[160px]" label="服务渠道">
|
||||
<el-select v-model="queryParams.service_channel" clearable placeholder="全部" class="!w-full">
|
||||
<el-option label="未指派" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<export-data
|
||||
v-perms="['tcm.prescriptionOrder/export']"
|
||||
class="ml-2.5"
|
||||
:fetch-fun="prescriptionOrderExport"
|
||||
:params="prescriptionOrderExportParams"
|
||||
:page-size="pager.size"
|
||||
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -426,7 +439,37 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="开方人" prop="doctor_name" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="创建人" prop="creator_name" width="90" show-overflow-tooltip />
|
||||
|
||||
<el-table-column min-width="128" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<span>二中心·指派</span>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
max-width="320"
|
||||
content="与诊单「指派记录」快照一致:仅当本条业务单在操作瞬间为快照中的「关联业务单」(创建人+创建时间命中)时显示。绿色=新医助在名称含「二中心」的部门及其下级;灰色=系统自动释放诊单医助。"
|
||||
>
|
||||
<el-icon class="ml-0.5 align-middle text-gray-400 cursor-help"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<template v-if="!Number(row.po_assign_snapshot_hit)">
|
||||
<span class="text-gray-400">—</span>
|
||||
</template>
|
||||
<template v-else-if="Number(row.po_assign_is_release)">
|
||||
<el-tag type="info" size="small">已释放</el-tag>
|
||||
</template>
|
||||
<template v-else-if="Number(row.po_assign_to_er_center)">
|
||||
<el-tag type="success" size="small">
|
||||
{{ row.po_assign_to_assistant_name || ('医助#' + row.po_assign_to_assistant_id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="warning" size="small" effect="plain">
|
||||
非二中心 {{ row.po_assign_to_assistant_name || ('医助#' + row.po_assign_to_assistant_id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="310" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-1 flex-wrap">
|
||||
@@ -519,6 +562,28 @@
|
||||
round
|
||||
class="ml-2"
|
||||
>甘草 {{ detailData.gancao_reciperl_order_no }}</el-tag>
|
||||
<div v-if="detailData" class="flex items-center gap-2 ml-4 shrink-0">
|
||||
<span class="text-sm text-gray-500">发货类型</span>
|
||||
<el-radio-group
|
||||
v-perms="['tcm.prescriptionOrder/setShipMode']"
|
||||
:model-value="normalizeShipMode(detailData.ship_mode)"
|
||||
size="small"
|
||||
:disabled="shipModeSaving || !canEditShipMode(detailData)"
|
||||
@change="onDetailShipModeChange"
|
||||
>
|
||||
<el-radio-button label="gancao">甘草药房</el-radio-button>
|
||||
<el-radio-button
|
||||
label="direct"
|
||||
:disabled="isShipModeLockedToGancao(detailData)"
|
||||
>洛阳药房</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-tag
|
||||
v-if="!hasSetShipModePerm"
|
||||
:type="normalizeShipMode(detailData.ship_mode) === 'direct' ? 'warning' : 'success'"
|
||||
effect="plain"
|
||||
round
|
||||
>{{ shipModeLabel(detailData.ship_mode) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailData" class="flex items-center gap-2 mr-6">
|
||||
<el-button
|
||||
@@ -621,6 +686,41 @@
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 物流轨迹含拒收/退回等:抽屉内强提示 -->
|
||||
<el-alert
|
||||
v-if="detailLogisticsUrgent.show && String(detailData?.tracking_number || '').trim()"
|
||||
type="error"
|
||||
effect="dark"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4 !items-start ring-2 ring-red-600/90 shadow-lg"
|
||||
title="物流异常:轨迹中出现拒收 / 退回等字样,请优先跟进处理"
|
||||
>
|
||||
<template #default>
|
||||
<div class="text-sm leading-relaxed space-y-1.5">
|
||||
<div v-if="detailLogisticsUrgent.keywords.length" class="flex flex-wrap gap-1.5 items-center">
|
||||
<span class="text-red-100 text-xs">命中关键词:</span>
|
||||
<el-tag
|
||||
v-for="k in detailLogisticsUrgent.keywords"
|
||||
:key="k"
|
||||
type="danger"
|
||||
effect="plain"
|
||||
size="small"
|
||||
class="!border-white/40 !text-white"
|
||||
>
|
||||
{{ k }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<ul v-if="detailLogisticsUrgent.lines.length" class="list-disc pl-5 space-y-1 text-red-50">
|
||||
<li v-for="(line, idx) in detailLogisticsUrgent.lines" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
<div class="text-xs text-red-100/90 pt-1 border-t border-white/15">
|
||||
请核实是否需改址重发、拦截退件或协调患者签收,并在备注中记录处理结果。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div class="bg-gray-50/80 border border-gray-100 rounded-lg p-5 mb-5 shadow-sm">
|
||||
<el-steps :active="workflowActiveStep" align-center finish-status="success">
|
||||
<el-step title="业务订单创建" :description="formatTime(detailData.create_time)" />
|
||||
@@ -911,11 +1011,12 @@
|
||||
<span class="text-red-500">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">
|
||||
{{ row.status_desc || orderStatusText(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 prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
@@ -968,11 +1069,12 @@
|
||||
<span class="text-red-500">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">
|
||||
{{ row.status_desc || orderStatusText(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 prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
@@ -1124,10 +1226,25 @@
|
||||
<el-card
|
||||
v-if="String(detailData.tracking_number || '').trim()"
|
||||
shadow="never"
|
||||
class="po-panel po-panel-logistics border-gray-100"
|
||||
class="po-panel po-panel-logistics"
|
||||
:class="
|
||||
detailLogisticsUrgent.show
|
||||
? '!border-2 !border-red-500 shadow-md shadow-red-500/10'
|
||||
: 'border-gray-100'
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<span class="font-medium text-[15px]">物流轨迹</span>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium text-[15px]">物流轨迹</span>
|
||||
<el-tag
|
||||
v-if="detailLogisticsUrgent.show"
|
||||
type="danger"
|
||||
effect="dark"
|
||||
size="small"
|
||||
>
|
||||
拒收/退回 · 待处理
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<p class="po-logistics-tip text-gray-400 text-xs mb-3 font-normal">
|
||||
优先读库,无记录时走快递100。顺丰会校验单号+电话(快递100要求完整手机号更易通过);默认预填订单收货手机,若仍提示验证码错误请改成与顺丰面单一致的号码。
|
||||
@@ -1198,7 +1315,14 @@
|
||||
:timestamp="t.time"
|
||||
placement="top"
|
||||
>
|
||||
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ t.context }}</span>
|
||||
<span
|
||||
:class="[
|
||||
idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500',
|
||||
logisticsTraceLineUrgent(t.context)
|
||||
? '!text-red-700 font-semibold bg-red-50 px-1.5 py-0.5 rounded'
|
||||
: ''
|
||||
]"
|
||||
>{{ t.context }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
|
||||
@@ -1280,7 +1404,7 @@
|
||||
<!-- 编辑(分步向导与「创建业务订单」弹窗统一) -->
|
||||
<el-dialog
|
||||
v-model="editVisible"
|
||||
title="编辑业务订单"
|
||||
:title="editGancaoLogisticsOnlyMode ? '修改物流信息(甘草订单)' : '编辑业务订单'"
|
||||
width="940px"
|
||||
top="4vh"
|
||||
:close-on-click-modal="false"
|
||||
@@ -1303,18 +1427,33 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<el-steps
|
||||
:active="editOrderStep"
|
||||
finish-status="success"
|
||||
align-center
|
||||
class="create-order-steps"
|
||||
<el-alert
|
||||
v-if="editGancaoLogisticsOnlyMode"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
title="订单已提交甘草,仅可修改快递单号与承运商;地址、金额等变更请先走取消流程。"
|
||||
>
|
||||
<el-step title="患者与收货" description="诊单与物流地址" />
|
||||
<el-step title="服务与支付单" description="套餐与关联收款" />
|
||||
<el-step title="金额与确认" description="费用与备注" />
|
||||
</el-steps>
|
||||
<template v-if="editGancaoDisplayNo" #default>
|
||||
<div class="text-sm text-gray-700 mt-1">甘草处方单号:{{ editGancaoDisplayNo }}</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<p class="create-order-step-lead">{{ editOrderStepLead }}</p>
|
||||
<template v-if="!editGancaoLogisticsOnlyMode">
|
||||
<el-steps
|
||||
:active="editOrderStep"
|
||||
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">{{ editOrderStepLead }}</p>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
@@ -1323,6 +1462,24 @@
|
||||
label-width="108px"
|
||||
class="create-order-form"
|
||||
>
|
||||
<template v-if="editGancaoLogisticsOnlyMode">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="物流设置">
|
||||
<div class="flex gap-2 w-full">
|
||||
<el-select v-model="editForm.express_company" placeholder="承运商" class="w-[140px] shrink-0">
|
||||
<el-option label="自动识别" value="auto" />
|
||||
<el-option label="顺丰速运" value="sf" />
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-input v-model="editForm.tracking_number" maxlength="80" placeholder="快递单号" class="flex-1 min-w-0" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<template v-if="!editGancaoLogisticsOnlyMode">
|
||||
<div v-show="editOrderStep === 0" class="create-order-step-panel">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
@@ -1469,7 +1626,17 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服务渠道">
|
||||
<el-input v-model="editForm.service_channel" placeholder="可选" maxlength="100" />
|
||||
<el-select
|
||||
v-model="editForm.service_channel"
|
||||
class="w-full"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="可选或自定义输入"
|
||||
>
|
||||
<el-option label="未指派" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -1646,12 +1813,13 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="create-order-footer">
|
||||
<div class="create-order-footer__hint">
|
||||
<div v-if="!editGancaoLogisticsOnlyMode" class="create-order-footer__hint">
|
||||
<span class="create-order-footer__step">第 {{ editOrderStep + 1 }} / 3 步</span>
|
||||
<span v-if="editOrderStep === 2 && editDepositMin > 0" class="create-order-footer__warn">
|
||||
订单金额与关联支付单合计须 ≥ ¥{{ formatMoney(editDepositMin) }}
|
||||
@@ -1659,17 +1827,19 @@
|
||||
</div>
|
||||
<div class="create-order-footer__actions">
|
||||
<el-button size="large" @click="editVisible = false">取消</el-button>
|
||||
<el-button v-if="editOrderStep > 0" size="large" @click="goEditOrderPrevStep">上一步</el-button>
|
||||
<template v-if="!editGancaoLogisticsOnlyMode">
|
||||
<el-button v-if="editOrderStep > 0" size="large" @click="goEditOrderPrevStep">上一步</el-button>
|
||||
<el-button
|
||||
v-if="editOrderStep < 2"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="goEditOrderNextStep"
|
||||
>
|
||||
下一步
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="editOrderStep < 2"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="goEditOrderNextStep"
|
||||
>
|
||||
下一步
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="editOrderStep === 2"
|
||||
v-if="editGancaoLogisticsOnlyMode || editOrderStep === 2"
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="editSaving"
|
||||
@@ -1777,10 +1947,12 @@
|
||||
/>
|
||||
<el-form v-loading="shipSaving" label-width="90px" class="pr-2" @submit.prevent="submitShip">
|
||||
<el-form-item label="发货方式">
|
||||
<el-radio-group v-model="shipForm.ship_mode">
|
||||
<el-radio label="gancao">甘草药方发</el-radio>
|
||||
<el-radio label="direct">药房直发</el-radio>
|
||||
</el-radio-group>
|
||||
<el-tag
|
||||
:type="normalizeShipMode(shipForm.ship_mode) === 'direct' ? 'warning' : 'success'"
|
||||
effect="plain"
|
||||
size="default"
|
||||
>{{ shipDialogModeDisplay }}</el-tag>
|
||||
<span class="text-xs text-gray-400 ml-2">请在订单详情顶部「发货类型」中设置,此处不可修改</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商">
|
||||
<el-select v-model="shipForm.express_company" class="w-full">
|
||||
@@ -1813,21 +1985,56 @@
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@closed="completeOrderId = 0"
|
||||
@closed="onCompleteOrderDialogClosed"
|
||||
>
|
||||
<p class="text-sm text-gray-600 mb-3">请选择本单结案状态。选择「已完成」时,实付将按已关联的支付单金额汇总。</p>
|
||||
<el-select
|
||||
v-model="completeFulfillmentStatus"
|
||||
class="w-full"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in completeOrderStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-loading="completeOrderLogisticsLoading" class="min-h-[52px]">
|
||||
<el-alert
|
||||
v-if="completeOrderLogisticsUrgent.show"
|
||||
type="error"
|
||||
effect="dark"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-3 !items-start ring-2 ring-red-600/70"
|
||||
title="物流异常:轨迹含拒收/退回等,请先核实处理再结案"
|
||||
>
|
||||
<template #default>
|
||||
<div class="text-xs leading-relaxed space-y-1.5">
|
||||
<div v-if="completeOrderLogisticsUrgent.keywords.length" class="flex flex-wrap gap-1 items-center">
|
||||
<span class="text-red-100/90">命中:</span>
|
||||
<el-tag
|
||||
v-for="k in completeOrderLogisticsUrgent.keywords"
|
||||
:key="k"
|
||||
type="danger"
|
||||
effect="plain"
|
||||
size="small"
|
||||
class="!border-white/35 !text-white"
|
||||
>
|
||||
{{ k }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<ul v-if="completeOrderLogisticsUrgent.lines.length" class="list-disc pl-4 space-y-0.5 text-red-50">
|
||||
<li v-for="(line, idx) in completeOrderLogisticsUrgent.lines" :key="idx">{{ line }}</li>
|
||||
</ul>
|
||||
<div class="text-red-100/85 border-t border-white/15 pt-1.5">
|
||||
确认已协调拒收/退回后续(重发、退款、改址等)后再完成订单。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
<p class="text-sm text-gray-600 mb-3">请选择本单结案状态。选择「已完成」时,实付将按已关联的支付单金额汇总。</p>
|
||||
<el-select
|
||||
v-model="completeFulfillmentStatus"
|
||||
class="w-full"
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in completeOrderStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="completeOrderDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="completeOrderSubmitting" @click="submitCompleteOrder">
|
||||
@@ -2163,7 +2370,7 @@
|
||||
<p v-if="rxPharmacyRemarkText" class="rx-text-warn">
|
||||
药房备注:{{ rxPharmacyRemarkText }}
|
||||
</p>
|
||||
<p v-if="rxOutPelletText" class="rx-text-warn">
|
||||
<p v-if="rxOutPelletText && prescriptionViewData?.prescription_type !== '饮片'" class="rx-text-warn">
|
||||
出丸:{{ rxOutPelletText }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -2373,7 +2580,7 @@
|
||||
<script lang="ts" setup name="prescriptionOrderList">
|
||||
import { computed, onMounted, reactive, ref, nextTick, watch, defineAsyncComponent } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
|
||||
import { Refresh, Loading, ArrowDown, InfoFilled, QuestionFilled, Search, Calendar, Document, Link as LinkIcon, Wallet } from '@element-plus/icons-vue'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import {
|
||||
prescriptionOrderAuditPayment,
|
||||
@@ -2381,12 +2588,14 @@ import {
|
||||
prescriptionOrderDetail,
|
||||
prescriptionOrderEdit,
|
||||
prescriptionOrderLists,
|
||||
prescriptionOrderExport,
|
||||
prescriptionOrderPaidPayOrders,
|
||||
prescriptionOrderWithdraw,
|
||||
prescriptionOrderLogisticsTrace,
|
||||
prescriptionOrderShip,
|
||||
prescriptionOrderLogs,
|
||||
prescriptionOrderUpdateAmount,
|
||||
prescriptionOrderSetShipMode,
|
||||
prescriptionOrderAddPayOrder,
|
||||
prescriptionOrderComplete,
|
||||
prescriptionOrderRevokeRxAudit,
|
||||
@@ -2665,6 +2874,8 @@ const queryParams = reactive({
|
||||
payment_slip_audit_status: '' as number | '',
|
||||
/** 供货方式:甘草(已传甘草药方单号)/ 自营(无甘草单号) */
|
||||
supply_mode: '' as '' | 'gancao' | 'self',
|
||||
/** 服务渠道:'' 不限;'0' 未指派(库内 '' 或 '0') */
|
||||
service_channel: '' as '' | '0',
|
||||
/** 列表排除履约已取消(4):重点看板「待处方审核」等用 */
|
||||
exclude_fulfillment_cancelled: 0 as number
|
||||
})
|
||||
@@ -2729,21 +2940,35 @@ function handleFocusBoardSearch(key: 'pendingRx' | 'pendingPay' | 'pendingShip'
|
||||
resetPage()
|
||||
}
|
||||
|
||||
async function fetchLists(params: Record<string, unknown>) {
|
||||
function normalizePrescriptionOrderListQuery(params: Record<string, unknown>): Record<string, unknown> {
|
||||
const p: Record<string, unknown> = { ...params }
|
||||
if (p.prescription_id === '' || p.prescription_id === undefined) {
|
||||
delete p.prescription_id
|
||||
} else {
|
||||
p.prescription_id = Number(p.prescription_id)
|
||||
}
|
||||
if (p.fulfillment_status === '' || p.fulfillment_status === undefined) {
|
||||
if (p.fulfillment_status === '' || p.fulfillment_status === undefined || p.fulfillment_status === null) {
|
||||
delete p.fulfillment_status
|
||||
} else {
|
||||
p.fulfillment_status = Number(p.fulfillment_status)
|
||||
}
|
||||
if (p.prescription_audit_status === '' || p.prescription_audit_status === undefined) {
|
||||
if (
|
||||
p.prescription_audit_status === '' ||
|
||||
p.prescription_audit_status === undefined ||
|
||||
p.prescription_audit_status === null
|
||||
) {
|
||||
delete p.prescription_audit_status
|
||||
} else {
|
||||
p.prescription_audit_status = Number(p.prescription_audit_status)
|
||||
}
|
||||
if (p.payment_slip_audit_status === '' || p.payment_slip_audit_status === undefined) {
|
||||
if (
|
||||
p.payment_slip_audit_status === '' ||
|
||||
p.payment_slip_audit_status === undefined ||
|
||||
p.payment_slip_audit_status === null
|
||||
) {
|
||||
delete p.payment_slip_audit_status
|
||||
} else {
|
||||
p.payment_slip_audit_status = Number(p.payment_slip_audit_status)
|
||||
}
|
||||
if (!p.supply_mode || p.supply_mode === '') {
|
||||
delete p.supply_mode
|
||||
@@ -2767,12 +2992,17 @@ async function fetchLists(params: Record<string, unknown>) {
|
||||
if (!String(p.patient_keyword || '').trim()) delete p.patient_keyword
|
||||
if (!String(p.express_keyword || '').trim()) delete p.express_keyword
|
||||
if (!p.express_company || p.express_company === '') delete p.express_company
|
||||
if (p.service_channel === '' || p.service_channel === undefined || p.service_channel === null) {
|
||||
delete p.service_channel
|
||||
} else {
|
||||
p.service_channel = String(p.service_channel).trim()
|
||||
if (p.service_channel === '') delete p.service_channel
|
||||
}
|
||||
if (Number(p.exclude_fulfillment_cancelled) !== 1) delete p.exclude_fulfillment_cancelled
|
||||
if (!String(p.start_time || '').trim() || !String(p.end_time || '').trim()) {
|
||||
delete p.start_time
|
||||
delete p.end_time
|
||||
} else {
|
||||
// 当前页面按“日”筛选:请求接口时展开为整天区间,避免结束日只落在 00:00:00。
|
||||
const st = String(p.start_time || '').trim()
|
||||
const et = String(p.end_time || '').trim()
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(st) && /^\d{4}-\d{2}-\d{2}$/.test(et)) {
|
||||
@@ -2780,9 +3010,16 @@ async function fetchLists(params: Record<string, unknown>) {
|
||||
p.end_time = `${et} 23:59:59`
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
async function fetchLists(params: Record<string, unknown>) {
|
||||
const p = normalizePrescriptionOrderListQuery({ ...params })
|
||||
return prescriptionOrderLists(p)
|
||||
}
|
||||
|
||||
const prescriptionOrderExportParams = computed(() => normalizePrescriptionOrderListQuery({ ...queryParams }))
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: fetchLists,
|
||||
params: queryParams
|
||||
@@ -2978,6 +3215,7 @@ function handleReset() {
|
||||
queryParams.prescription_audit_status = ''
|
||||
queryParams.payment_slip_audit_status = ''
|
||||
queryParams.supply_mode = ''
|
||||
queryParams.service_channel = ''
|
||||
queryParams.exclude_fulfillment_cancelled = 0
|
||||
resetParams()
|
||||
}
|
||||
@@ -3081,14 +3319,21 @@ function canEditRow(row: {
|
||||
gancao_reciperl_order_no?: string | null
|
||||
gancao_submit_time?: number | null
|
||||
}) {
|
||||
// 已发货(5)、已签收(6)、已完成(3)、已取消(4) 不可编辑
|
||||
const fs = Number(row.fulfillment_status)
|
||||
if (fs !== 1 && fs !== 2) return false
|
||||
// 已成功提交甘草(已生成甘草处方单号 或 提交时间 > 0)不允许再编辑
|
||||
// 已完成(3)、已取消(4) 不可编辑
|
||||
if (fs === 3 || fs === 4) return false
|
||||
|
||||
const gcNo = String(row.gancao_reciperl_order_no || '').trim()
|
||||
const gcTime = Number(row.gancao_submit_time || 0)
|
||||
if (gcNo !== '' || gcTime > 0) return false
|
||||
return true
|
||||
const gcLocked = gcNo !== '' || gcTime > 0
|
||||
|
||||
// 甘草已提交:仅允许在待双审/履约中/已发货/进行中下修改快递(已签收 6 不可改单号;后端 edit 亦拦截)
|
||||
if (gcLocked) {
|
||||
return [1, 2, 5, 7].includes(fs)
|
||||
}
|
||||
|
||||
// 未提交甘草:仅待双审(1)、履约中(2)可全量编辑
|
||||
return fs === 1 || fs === 2
|
||||
}
|
||||
|
||||
function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) {
|
||||
@@ -3144,6 +3389,70 @@ function canShipRow(row: { fulfillment_status?: number }) {
|
||||
return Number(row.fulfillment_status) === 2
|
||||
}
|
||||
|
||||
type ShipMode = 'gancao' | 'direct'
|
||||
|
||||
const hasSetShipModePerm = computed(() => {
|
||||
const p = userStore.perms || []
|
||||
return p.includes('*') || p.includes('tcm.prescriptionOrder/setShipMode')
|
||||
})
|
||||
|
||||
function normalizeShipMode(v: unknown): ShipMode {
|
||||
return String(v || '').toLowerCase() === 'direct' ? 'direct' : 'gancao'
|
||||
}
|
||||
|
||||
function shipModeLabel(v: unknown): string {
|
||||
return normalizeShipMode(v) === 'direct' ? '洛阳药房' : '甘草药房'
|
||||
}
|
||||
|
||||
function isShipModeLockedToGancao(row: { gancao_reciperl_order_no?: string | null }) {
|
||||
return String(row.gancao_reciperl_order_no || '').trim() !== ''
|
||||
}
|
||||
|
||||
function canEditShipMode(row: { fulfillment_status?: number }) {
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs !== 3 && fs !== 4
|
||||
}
|
||||
|
||||
const shipModeSaving = ref(false)
|
||||
|
||||
function isSamePrescriptionOrderId(a: unknown, b: unknown) {
|
||||
const na = Number(a)
|
||||
const nb = Number(b)
|
||||
return na > 0 && na === nb
|
||||
}
|
||||
|
||||
const shipDialogModeDisplay = computed(() => {
|
||||
return normalizeShipMode(shipForm.ship_mode) === 'direct' ? '洛阳药房直发' : '甘草药房直发'
|
||||
})
|
||||
|
||||
async function onDetailShipModeChange(mode: string | number | boolean | undefined) {
|
||||
const row = detailData.value
|
||||
if (!row?.id) return
|
||||
const next = normalizeShipMode(mode)
|
||||
const prev = normalizeShipMode(row.ship_mode)
|
||||
if (next === prev) return
|
||||
if (!canEditShipMode(row)) {
|
||||
feedback.msgWarning('当前订单状态不可修改发货类型')
|
||||
return
|
||||
}
|
||||
shipModeSaving.value = true
|
||||
try {
|
||||
await prescriptionOrderSetShipMode({ id: row.id, ship_mode: next })
|
||||
if (detailData.value) detailData.value.ship_mode = next
|
||||
if (shipVisible.value && isSamePrescriptionOrderId(shipRowId.value, row.id)) {
|
||||
shipForm.ship_mode = next
|
||||
}
|
||||
await refreshCurrentPrescriptionOrderDetail()
|
||||
feedback.msgSuccess('发货类型已保存')
|
||||
getLists()
|
||||
void fetchLogs(row.id)
|
||||
} catch {
|
||||
/* 拦截器已提示 */
|
||||
} finally {
|
||||
shipModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function canAddPayOrderRow(row: { fulfillment_status?: number }) {
|
||||
// 已发货(5) / 已签收(6) 状态可补齐支付单
|
||||
const fs = Number(row.fulfillment_status)
|
||||
@@ -3157,9 +3466,8 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
|
||||
}
|
||||
|
||||
function canQuickTrackRow(row: { fulfillment_status?: number }) {
|
||||
// 已发货(5) / 已签收(6) 后如需修改快递信息可单独使用此功能
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs === 5 || fs === 6
|
||||
// 仅已发货(5) 可修改快递单号;已签收(6) 不可再改
|
||||
return Number(row.fulfillment_status) === 5
|
||||
}
|
||||
|
||||
function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) {
|
||||
@@ -3532,6 +3840,75 @@ const logisticsTraceList = computed(() => {
|
||||
return p.traces as Array<{ time: string; context: string }>
|
||||
})
|
||||
|
||||
/** 物流轨迹中需人工立即跟进的常见异常关键词 */
|
||||
const LOGISTICS_URGENT_KEYWORDS = [
|
||||
'拒收',
|
||||
'拒签',
|
||||
'退件',
|
||||
'客户拒收',
|
||||
'拦截退回',
|
||||
'退回发件',
|
||||
'派件退回',
|
||||
'无人签收',
|
||||
'退回快件'
|
||||
]
|
||||
|
||||
function logisticsStringHasUrgentKeyword(s: unknown): boolean {
|
||||
const t = String(s ?? '')
|
||||
return LOGISTICS_URGENT_KEYWORDS.some((k) => t.includes(k))
|
||||
}
|
||||
|
||||
function logisticsTraceLineUrgent(context: unknown): boolean {
|
||||
return logisticsStringHasUrgentKeyword(context)
|
||||
}
|
||||
|
||||
/** 根据单次拉取的物流 payload 判断是否含拒收/退回等(详情抽屉、完成订单弹窗共用) */
|
||||
function analyzeLogisticsPayloadUrgent(p: Record<string, any> | null | undefined): {
|
||||
show: boolean
|
||||
keywords: string[]
|
||||
lines: string[]
|
||||
} {
|
||||
if (!p) {
|
||||
return { show: false, keywords: [], lines: [] }
|
||||
}
|
||||
const traces = Array.isArray(p.traces)
|
||||
? (p.traces as Array<{ time?: string; context?: string }>)
|
||||
: []
|
||||
const chunks: string[] = []
|
||||
if (p.state_text) chunks.push(String(p.state_text))
|
||||
if (p.hint) chunks.push(String(p.hint))
|
||||
for (const row of traces) {
|
||||
if (row.context) chunks.push(String(row.context))
|
||||
}
|
||||
const joined = chunks.join('\n')
|
||||
const keywords = LOGISTICS_URGENT_KEYWORDS.filter((k) => joined.includes(k))
|
||||
if (!keywords.length) {
|
||||
return { show: false, keywords: [], lines: [] }
|
||||
}
|
||||
const lines: string[] = []
|
||||
for (const row of traces) {
|
||||
const ctx = String(row.context || '')
|
||||
if (logisticsStringHasUrgentKeyword(ctx)) {
|
||||
lines.push(ctx)
|
||||
if (lines.length >= 5) break
|
||||
}
|
||||
}
|
||||
if (!lines.length && logisticsStringHasUrgentKeyword(p.state_text)) {
|
||||
lines.push(`最新状态:${p.state_text}`)
|
||||
}
|
||||
if (!lines.length && logisticsStringHasUrgentKeyword(p.hint)) {
|
||||
lines.push(String(p.hint))
|
||||
}
|
||||
return { show: true, keywords, lines }
|
||||
}
|
||||
|
||||
/** 详情抽屉:轨迹/状态中是否出现拒收等字样,用于顶部与物流卡片强提示 */
|
||||
const detailLogisticsUrgent = computed(() => analyzeLogisticsPayloadUrgent(logisticsTracePayload.value))
|
||||
|
||||
const completeOrderLogisticsPayload = ref<Record<string, any> | null>(null)
|
||||
const completeOrderLogisticsLoading = ref(false)
|
||||
const completeOrderLogisticsUrgent = computed(() => analyzeLogisticsPayloadUrgent(completeOrderLogisticsPayload.value))
|
||||
|
||||
function expressCompanyLabel(v: unknown) {
|
||||
const s = String(v || '').toLowerCase()
|
||||
if (s === 'sf') return '顺丰速运'
|
||||
@@ -3726,6 +4103,10 @@ const editSaving = ref(false)
|
||||
const editOrderStep = ref(0)
|
||||
/** 顶部「关联处方」卡片数据(来自业务订单详情中的 prescription) */
|
||||
const editOrderPrescription = ref<Record<string, any> | null>(null)
|
||||
/** 甘草 SCM 已提交:弹窗仅展示并提交快递单号与承运商 */
|
||||
const editGancaoLogisticsOnlyMode = ref(false)
|
||||
/** 用于提示文案展示甘草处方单号 */
|
||||
const editGancaoDisplayNo = ref('')
|
||||
|
||||
const editOrderStepLead = computed(() => {
|
||||
const texts = [
|
||||
@@ -3904,6 +4285,8 @@ const editRules = computed<FormRules>(() => {
|
||||
function resetEditOrderDialog() {
|
||||
editOrderStep.value = 0
|
||||
editOrderPrescription.value = null
|
||||
editGancaoLogisticsOnlyMode.value = false
|
||||
editGancaoDisplayNo.value = ''
|
||||
editFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -4023,7 +4406,12 @@ async function openEdit(row: { id: number }) {
|
||||
editForm.pay_order_ids = Array.isArray(pids) ? pids.map((x: unknown) => Number(x)).filter((n) => !Number.isNaN(n)) : []
|
||||
editForm.internal_cost =
|
||||
d.internal_cost != null && d.internal_cost !== '' ? Number(d.internal_cost) : undefined
|
||||
|
||||
|
||||
const gcNo = String(d.gancao_reciperl_order_no || '').trim()
|
||||
const gcTime = Number(d.gancao_submit_time || 0)
|
||||
editGancaoLogisticsOnlyMode.value = gcNo !== '' || gcTime > 0
|
||||
editGancaoDisplayNo.value = gcNo || ''
|
||||
|
||||
await loadEditPaidOrders(editForm.diagnosis_id, editForm.id)
|
||||
|
||||
// BUG FIX: NextTick 清理刚展开时意外触发的金额下限表单校验红字
|
||||
@@ -4039,8 +4427,14 @@ async function openEdit(row: { id: number }) {
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editFormRef.value || editOrderStep.value !== 2) return
|
||||
await editFormRef.value.validate()
|
||||
if (!editFormRef.value) return
|
||||
const gancaoLogisticsOnly = editGancaoLogisticsOnlyMode.value
|
||||
if (!gancaoLogisticsOnly && editOrderStep.value !== 2) return
|
||||
if (!gancaoLogisticsOnly) {
|
||||
await editFormRef.value.validate()
|
||||
} else {
|
||||
editFormRef.value.clearValidate()
|
||||
}
|
||||
editSaving.value = true
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -4242,6 +4636,61 @@ async function testGancaoPreviewFromDetail() {
|
||||
if (d && d.success) {
|
||||
gancaoPreviewData.value = d
|
||||
gancaoPreviewDrawerVisible.value = true
|
||||
|
||||
// 将价格信息「总计」(药材成本 + 制作费 + 物流费) 写入 internal_cost(需财务字段权限,与后台 canViewInternalCost 一致)
|
||||
if (canViewFinanceFields() && d.fee) {
|
||||
const internalTotal = calculateTotalFee(d.fee)
|
||||
try {
|
||||
const resDetail: any = await prescriptionOrderDetail({ id: detailData.value.id })
|
||||
const od = resDetail?.data ?? resDetail
|
||||
if (od?.id) {
|
||||
const pkgRaw = od.service_package
|
||||
const servicePackageStr = Array.isArray(pkgRaw)
|
||||
? pkgRaw.join(',')
|
||||
: String(pkgRaw || '')
|
||||
await prescriptionOrderEdit({
|
||||
id: od.id,
|
||||
recipient_name: od.recipient_name || '',
|
||||
recipient_phone: od.recipient_phone || '',
|
||||
shipping_province: od.shipping_province || '',
|
||||
shipping_city: od.shipping_city || '',
|
||||
shipping_district: od.shipping_district || '',
|
||||
shipping_address: od.shipping_address || '',
|
||||
is_follow_up: od.is_follow_up ? 1 : 0,
|
||||
medication_days:
|
||||
od.medication_days != null && String(od.medication_days).trim() !== ''
|
||||
? od.medication_days
|
||||
: '',
|
||||
dose_unit: od.dose_unit || '剂',
|
||||
dose_count: od.dose_count > 0 ? od.dose_count : 1,
|
||||
prev_staff: od.prev_staff || '',
|
||||
service_channel: od.service_channel || '',
|
||||
service_package: servicePackageStr,
|
||||
express_company: od.express_company || 'auto',
|
||||
tracking_number: od.tracking_number || '',
|
||||
fee_type: Number(od.fee_type) || 3,
|
||||
amount: Number(od.amount) || 0,
|
||||
remark_extra: od.remark_extra || '',
|
||||
remark_assistant: od.remark_assistant || '',
|
||||
internal_cost: internalTotal,
|
||||
pay_order_ids: Array.isArray(od.pay_order_ids) ? od.pay_order_ids : []
|
||||
})
|
||||
feedback.msgSuccess('内部成本已按预报价总计更新')
|
||||
getLists()
|
||||
if (detailVisible.value && Number(detailData.value?.id) === Number(od.id)) {
|
||||
try {
|
||||
const r: any = await prescriptionOrderDetail({ id: od.id })
|
||||
const nd = r?.data ?? r ?? null
|
||||
if (nd) detailData.value = nd
|
||||
} catch {
|
||||
detailData.value = { ...detailData.value, internal_cost: internalTotal }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 拦截器已提示;仍保留抽屉中的预报价结果 */
|
||||
}
|
||||
}
|
||||
//feedback.msgSuccess('预下单测试成功')
|
||||
} else {
|
||||
// feedback.msgError(d?.error || '预下单失败')
|
||||
@@ -4357,8 +4806,8 @@ async function submitQuickTrack() {
|
||||
if (nd) detailData.value = nd
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
// 履约中状态且刚填单号,提示是否立即发货
|
||||
if (canShipRow({ fulfillment_status: 2, tracking_number: quickTrackForm.tracking_number })) {
|
||||
// 仅保存前为履约中(2)时才询问确认发货;已发货(5)/已签收(6)等修改单号不再弹窗
|
||||
if (canShipRow({ fulfillment_status: Number(d.fulfillment_status) })) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`,
|
||||
@@ -4385,14 +4834,31 @@ const shipForm = reactive({
|
||||
tracking_number: ''
|
||||
})
|
||||
|
||||
function resolveShipModeForRow(row: { id: number; ship_mode?: unknown }) {
|
||||
if (isSamePrescriptionOrderId(detailData.value?.id, row.id)) {
|
||||
return normalizeShipMode(detailData.value!.ship_mode)
|
||||
}
|
||||
return normalizeShipMode(row.ship_mode)
|
||||
}
|
||||
|
||||
function openShip(row: { id: number; express_company?: unknown; tracking_number?: unknown; ship_mode?: unknown }) {
|
||||
shipRowId.value = row.id
|
||||
shipForm.ship_mode = String(row.ship_mode || 'gancao') || 'gancao'
|
||||
shipRowId.value = Number(row.id)
|
||||
shipForm.ship_mode = resolveShipModeForRow(row)
|
||||
shipForm.express_company = String(row.express_company || 'auto') || 'auto'
|
||||
shipForm.tracking_number = String(row.tracking_number || '')
|
||||
shipVisible.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => detailData.value?.ship_mode,
|
||||
(v) => {
|
||||
if (!shipVisible.value || !shipRowId.value || !isSamePrescriptionOrderId(detailData.value?.id, shipRowId.value)) {
|
||||
return
|
||||
}
|
||||
shipForm.ship_mode = normalizeShipMode(v)
|
||||
}
|
||||
)
|
||||
|
||||
async function submitShip() {
|
||||
if (!shipForm.tracking_number.trim()) {
|
||||
feedback.msgError('请先填写快递单号再确认发货')
|
||||
@@ -4402,7 +4868,7 @@ async function submitShip() {
|
||||
try {
|
||||
await prescriptionOrderShip({
|
||||
id: shipRowId.value,
|
||||
ship_mode: shipForm.ship_mode === 'direct' ? 'direct' : 'gancao',
|
||||
ship_mode: normalizeShipMode(shipForm.ship_mode),
|
||||
express_company: shipForm.express_company || 'auto',
|
||||
tracking_number: shipForm.tracking_number.trim()
|
||||
})
|
||||
@@ -4439,6 +4905,37 @@ const completeOrderId = ref(0)
|
||||
const completeFulfillmentStatus = ref<number>(3)
|
||||
const completeOrderSubmitting = ref(false)
|
||||
|
||||
function onCompleteOrderDialogClosed() {
|
||||
completeOrderId.value = 0
|
||||
completeOrderLogisticsPayload.value = null
|
||||
completeOrderLogisticsLoading.value = false
|
||||
}
|
||||
|
||||
async function fetchCompleteOrderLogisticsForDialog(row: {
|
||||
id: number
|
||||
express_company?: unknown
|
||||
recipient_phone?: unknown
|
||||
}) {
|
||||
completeOrderLogisticsLoading.value = true
|
||||
completeOrderLogisticsPayload.value = null
|
||||
try {
|
||||
const digits = String(row.recipient_phone || '').replace(/\D/g, '')
|
||||
const params: { id: number; express_company?: string; phone_tail?: string } = {
|
||||
id: row.id,
|
||||
express_company: String(row.express_company || 'auto') || 'auto'
|
||||
}
|
||||
if (digits.length >= 4) {
|
||||
params.phone_tail = digits
|
||||
}
|
||||
const res: any = await prescriptionOrderLogisticsTrace(params)
|
||||
completeOrderLogisticsPayload.value = (res?.data ?? res) as Record<string, any>
|
||||
} catch {
|
||||
completeOrderLogisticsPayload.value = null
|
||||
} finally {
|
||||
completeOrderLogisticsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCompleteOrder() {
|
||||
const id = completeOrderId.value
|
||||
if (!id) return
|
||||
@@ -4601,10 +5098,19 @@ async function submitAddPayOrder() {
|
||||
}
|
||||
|
||||
// ─── 完成订单(已发货/已签收 + 支付审核通过)────────────────────────────────────────────
|
||||
function confirmComplete(row: { id: number }) {
|
||||
function confirmComplete(row: {
|
||||
id: number
|
||||
tracking_number?: unknown
|
||||
express_company?: unknown
|
||||
recipient_phone?: unknown
|
||||
}) {
|
||||
completeOrderId.value = row.id
|
||||
completeFulfillmentStatus.value = 3
|
||||
completeOrderLogisticsPayload.value = null
|
||||
completeOrderDialogVisible.value = true
|
||||
if (String(row.tracking_number || '').trim()) {
|
||||
void fetchCompleteOrderLogisticsForDialog(row)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 撤回处方审核 ────────────────────────────────────────────
|
||||
@@ -4793,6 +5299,8 @@ const rxTypeText = computed(() => {
|
||||
const v = prescriptionViewData.value as any
|
||||
if (!v) return '—'
|
||||
const t = v.prescription_type || '浓缩水丸'
|
||||
/** 饮片不匹配「丸|散|膏|片」,避免误显示「浓缩丸-饮片」;类型字段直接展示饮片 */
|
||||
if (t === '饮片') return '饮片'
|
||||
return /丸|散|膏|片/.test(t) ? `浓缩丸-${t}` : t
|
||||
})
|
||||
|
||||
|
||||
@@ -925,11 +925,12 @@
|
||||
<span class="text-red-500">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">
|
||||
{{ row.status_desc || orderStatusText(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 prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
@@ -982,11 +983,12 @@
|
||||
<span class="text-red-500">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">
|
||||
{{ row.status_desc || orderStatusText(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 prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
@@ -3050,14 +3052,18 @@ function canEditRow(row: {
|
||||
gancao_reciperl_order_no?: string | null
|
||||
gancao_submit_time?: number | null
|
||||
}) {
|
||||
// 已发货(5)、已签收(6)、已完成(3)、已取消(4) 不可编辑
|
||||
const fs = Number(row.fulfillment_status)
|
||||
if (fs !== 1 && fs !== 2) return false
|
||||
// 已成功提交甘草(已生成甘草处方单号 或 提交时间 > 0)不允许再编辑
|
||||
if (fs === 3 || fs === 4) return false
|
||||
|
||||
const gcNo = String(row.gancao_reciperl_order_no || '').trim()
|
||||
const gcTime = Number(row.gancao_submit_time || 0)
|
||||
if (gcNo !== '' || gcTime > 0) return false
|
||||
return true
|
||||
const gcLocked = gcNo !== '' || gcTime > 0
|
||||
|
||||
if (gcLocked) {
|
||||
return [1, 2, 5, 7].includes(fs)
|
||||
}
|
||||
|
||||
return fs === 1 || fs === 2
|
||||
}
|
||||
|
||||
function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) {
|
||||
@@ -3126,9 +3132,7 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s
|
||||
}
|
||||
|
||||
function canQuickTrackRow(row: { fulfillment_status?: number }) {
|
||||
// 已发货(5) / 已签收(6) 后如需修改快递信息可单独使用此功能
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs === 5 || fs === 6
|
||||
return Number(row.fulfillment_status) === 5
|
||||
}
|
||||
|
||||
function canUploadGancaoRow(row: {
|
||||
@@ -4310,8 +4314,8 @@ async function submitQuickTrack() {
|
||||
if (nd) detailData.value = nd
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
// 履约中状态且刚填单号,提示是否立即发货
|
||||
if (canShipRow({ fulfillment_status: 2, tracking_number: quickTrackForm.tracking_number })) {
|
||||
// 仅保存前为履约中(2)时才询问确认发货;已发货(5)/已签收(6)等修改单号不再弹窗
|
||||
if (canShipRow({ fulfillment_status: Number(d.fulfillment_status) })) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`快递单号「${quickTrackForm.tracking_number}」已保存,是否立即确认发货?`,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,6 +102,18 @@
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="添加时间">
|
||||
<el-date-picker
|
||||
v-model="addTimeRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
clearable
|
||||
class="!w-[280px]"
|
||||
@change="onAddTimeRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select
|
||||
v-model="queryParams.tag_ids"
|
||||
@@ -557,12 +569,34 @@ const syncSettings = reactive({
|
||||
interval: 3600
|
||||
})
|
||||
|
||||
const queryParams = reactive<{ name: string; follow_user: string; tag_ids: string[] }>({
|
||||
const queryParams = reactive<{
|
||||
name: string
|
||||
follow_user: string
|
||||
tag_ids: string[]
|
||||
add_time_start: string
|
||||
add_time_end: string
|
||||
}>({
|
||||
name: '',
|
||||
follow_user: '',
|
||||
tag_ids: []
|
||||
tag_ids: [],
|
||||
add_time_start: '',
|
||||
add_time_end: ''
|
||||
})
|
||||
|
||||
/** 与列表「添加时间」列口径一致(库内 external_first_add_time / create_time) */
|
||||
const addTimeRange = ref<[string, string] | null>(null)
|
||||
|
||||
function onAddTimeRangeChange(val: [string, string] | null) {
|
||||
if (val && val.length === 2 && val[0] && val[1]) {
|
||||
queryParams.add_time_start = val[0]
|
||||
queryParams.add_time_end = val[1]
|
||||
} else {
|
||||
queryParams.add_time_start = ''
|
||||
queryParams.add_time_end = ''
|
||||
}
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// ── 标签维度(筛选下拉 + 抽屉面板共用同一份数据) ──────────────────────────
|
||||
interface TagItem {
|
||||
tag_id: string
|
||||
@@ -793,6 +827,9 @@ function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.follow_user = ''
|
||||
queryParams.tag_ids = []
|
||||
queryParams.add_time_start = ''
|
||||
queryParams.add_time_end = ''
|
||||
addTimeRange.value = null
|
||||
resetParams()
|
||||
}
|
||||
|
||||
|
||||
+438
-246
@@ -290,12 +290,12 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="yeji-table__foot yeji-table__foot--left">
|
||||
<span>诊金单位:元 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线(元/进线)</span>
|
||||
<span>诊金单位:元 · 诊金/接诊诊单/复诊均按「订单创建人 = 该医助」归属,与部门「合计业绩 / 接诊诊单 / 复诊」同口径 · 被指派数与部门业绩表同口径 · 接诊率 = 诊金 ÷ 进线(元/进线)</span>
|
||||
<span v-if="lb.er_center_subtree">
|
||||
· 复诊列为二中心口径(同一诊单业务单序列第 2 笔起),点击数字查看对应业务订单
|
||||
</span>
|
||||
<span>
|
||||
· 预约诊单:预约日期在统计区间内,状态含已预约/已完成/已过号(不含已取消),医助归属与接诊单数一致;各科组展示全部医助。
|
||||
· 预约诊单:预约日期在统计区间内,状态含已预约/已完成/已过号(不含已取消),医助归属与接诊单数一致(按挂号 assistant_id 优先,否则诊单 assistant_id);各科组展示全部医助。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,75 +317,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||
<p class="yeji-zyyt-intro">
|
||||
各中心部门诊金、业绩、进线与 ROI 等,与顶部渠道、展示部门、日期筛选联动。本月 / 本周 / 今日等多张业绩表中「预约诊单」列数字可点开查看每条挂号记录。
|
||||
</p>
|
||||
|
||||
<!-- <el-alert
|
||||
v-if="channelFilterNote"
|
||||
:title="channelFilterNote"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="filter-alert"
|
||||
/> -->
|
||||
<!-- ── 目标进度卡片 ── -->
|
||||
<div
|
||||
v-if="targetProgressCard || targetProgressLoading"
|
||||
v-loading="targetProgressLoading"
|
||||
class="tp-wrap"
|
||||
>
|
||||
<div v-if="targetProgressCard" class="tp-card">
|
||||
<div class="tp-card__title">{{ targetProgressCard.title }}</div>
|
||||
<div class="tp-card__body">
|
||||
<div
|
||||
v-for="sec in targetProgressCard.sections"
|
||||
:key="sec.dept_id"
|
||||
class="tp-section"
|
||||
>
|
||||
<div class="tp-section__head">
|
||||
<div class="tp-section__title-col">
|
||||
<span v-if="sec.parent_dept_name" class="tp-section__parent">{{
|
||||
sec.parent_dept_name
|
||||
}}</span>
|
||||
<span class="tp-section__name">{{ sec.dept_name }}目标</span>
|
||||
</div>
|
||||
<span class="tp-section__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
||||
</div>
|
||||
<table class="tp-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tp-lbl">昨日诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.yesterday_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.douyin_amount !== null">
|
||||
<td class="tp-lbl">抖音总诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.douyin_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.tuifen_amount !== null">
|
||||
<td class="tp-lbl">推粉总诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.tuifen_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">总诊金</td>
|
||||
<td class="tp-val tp-val--total">{{ formatInt(sec.total_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">目标进度</td>
|
||||
<td class="tp-val">{{ formatMoney(sec.target_progress) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">完成进度</td>
|
||||
<td class="tp-val" :class="achievementClass(sec.completion_rate)">{{ formatAchievement(sec.completion_rate) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 业绩表(4 张/N 张) ────────────────── -->
|
||||
<!-- ── 业绩表(4 张/N 张);目标看板见「目标看板(卡片)」Tab ── -->
|
||||
<div v-loading="loading" class="tables-wrap">
|
||||
<div v-if="!loading && tables.length === 0" class="empty-tip">
|
||||
<el-empty description="暂无数据" />
|
||||
@@ -438,7 +370,7 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
仅<b>名称含「二中心」</b>的部门及其组织下级:与本表区间一致,按患者业务单 <b>create_time</b> 排序,<b>第 1 笔不计</b>;第 2 笔起计入合计,并分列复诊2、复诊三…(含本部门及下级汇总)。订单条件同「合计业绩」列(非取消)。非二中心子树部门为 0。
|
||||
仅<b>名称含「二中心」</b>的部门及其组织下级:候选订单为有诊单医助、且 <b>fulfillment_status</b> 非 4/9/10(<b>NULL 计入</b>)的业务单,按患者 <b>create_time</b> 排序,<b>第 1 笔不计</b>;第 2 笔起计入合计,分列复诊2、复诊三…。<b>归属</b>按订单创建人的人事部门,与<b>合计业绩</b>列同口径。非二中心子树部门为 0。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -463,8 +395,9 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
|
||||
按订单<b>创建时间</b>统计(剔除履约已取消),金额与<b>接诊诊单</b>列一致:诊单<b>医助</b>或订单<b>创建人</b>的人事部门落在本展示行(含下级)即计入,多行命中时取最深的展示部门。<br />
|
||||
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表可对齐。
|
||||
按订单<b>创建时间</b>统计(剔除履约已取消/拒收/退款 4·9·10),金额与<b>接诊诊单</b>列一致:仅按<b>订单创建人</b>的人事部门落在本展示行(含下级)计入,多行命中时取最深的展示部门。与<b>医助排行榜诊金</b>同口径。<br />
|
||||
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表对齐。<br />
|
||||
点击金额可查看对应业务订单明细(侧栏默认「合计业绩」全量列表)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -476,8 +409,9 @@
|
||||
<template #content>
|
||||
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
|
||||
「{{ tb.channel_name }}」渠道业绩(与卡片标题渠道一致)。与「处方订单列表」业绩<b>同口径</b>:按订单
|
||||
<b>create_time</b> 归日,剔除履约已取消(4)。<br />
|
||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。
|
||||
<b>create_time</b> 归日,剔除履约状态 4(已取消)/9(拒收)/10(退款)。<br />
|
||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。<br />
|
||||
点击金额可查看该部门本渠道订单明细(侧栏「渠道筛选」列表)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||
@@ -511,8 +445,12 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
<b>业务订单条数</b>(非取消):订单 <b>create_time</b> 落入区间、<b>fulfillment_status ≠ 4</b>;与列表筛选
|
||||
<b>assistant_dept_id</b> 时一致——诊单 <b>医助</b>或订单 <b>创建人</b>的人事部门落在该部门子树即计入(表格多行命中时取最深的展示部门)。与侧栏勾选「与表格业绩对齐」时的集合可能略有差异。选定渠道时本列仍为全量。
|
||||
<<<<<<< HEAD
|
||||
<b>业务订单条数</b>(计业绩):订单 <b>create_time</b> 落入区间、<b>fulfillment_status ∉ {4,9,10}</b>(<b>NULL 计入</b>);按<b>订单创建人</b>的人事部门落在该部门子树即计入(表格多行命中时取最深的展示部门)。与<b>合计业绩 / 医助排行榜接诊诊单</b>同口径。选定渠道时本列仍为全量。
|
||||
=======
|
||||
<b>业务订单条数</b>(计业绩):订单 <b>create_time</b> 落入区间、<b>fulfillment_status ∉ {4,9,10}</b>;与列表筛选
|
||||
<b>assistant_dept_id</b> 时一致——<b>创建人</b>人事部门优先,无创建人则诊单 <b>医助</b>;落在该部门子树即计入(表格多行命中时取最深的展示部门)。与侧栏勾选「与表格业绩对齐」时的集合可能略有差异。选定渠道时本列仍为全量。
|
||||
>>>>>>> master
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -523,7 +461,7 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
「{{ tb.channel_name }}」渠道成交单数;与渠道业绩同源:区间内 <b>非取消业务订单</b> 条数,按订单
|
||||
「{{ tb.channel_name }}」渠道成交单数;与渠道业绩同源:区间内 <b>计业绩的业务订单</b> 条数(剔除 4/9/10),按订单
|
||||
<b>create_time</b> 归日;渠道判定同业绩列。
|
||||
</div>
|
||||
</template>
|
||||
@@ -611,8 +549,25 @@
|
||||
>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
|
||||
<template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
|
||||
</td>
|
||||
<td>{{ formatMoney(r.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="yejiPerfCellDrawerEligible(tb, r, 'total')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceCellClick(tb, r, 'total')"
|
||||
>{{ formatMoney(r.performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(r.performance_amount) }}</template>
|
||||
</td>
|
||||
<td
|
||||
v-if="tb.channel_name"
|
||||
class="col-completed"
|
||||
>
|
||||
<span
|
||||
v-if="yejiPerfCellDrawerEligible(tb, r, 'channel')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceCellClick(tb, r, 'channel')"
|
||||
>{{ formatMoney(r.completed_performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(r.completed_performance_amount) }}</template>
|
||||
</td>
|
||||
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
|
||||
<span
|
||||
v-if="
|
||||
@@ -639,8 +594,25 @@
|
||||
v-for="slot in yejiRevisitSlotColumns"
|
||||
:key="'yjvt-' + idx + '-' + slot"
|
||||
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
|
||||
<td>{{ formatMoney(tb.total.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(tb.total.completed_performance_amount) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="yejiPerfTotalRowDrawerEligible(tb, 'total')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceTotalRowClick(tb, 'total')"
|
||||
>{{ formatMoney(tb.total.performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(tb.total.performance_amount) }}</template>
|
||||
</td>
|
||||
<td
|
||||
v-if="tb.channel_name"
|
||||
class="col-completed"
|
||||
>
|
||||
<span
|
||||
v-if="yejiPerfTotalRowDrawerEligible(tb, 'channel')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceTotalRowClick(tb, 'channel')"
|
||||
>{{ formatMoney(tb.total.completed_performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(tb.total.completed_performance_amount) }}</template>
|
||||
</td>
|
||||
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
|
||||
<td>{{ formatInt(tb.total.consult_count) }}</td>
|
||||
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
|
||||
@@ -663,6 +635,72 @@
|
||||
</div>
|
||||
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="目标看板(卡片)" name="zyyt_matrix" lazy>
|
||||
<div v-loading="targetProgressLoading" class="tp-matrix-wrap">
|
||||
<template v-if="targetProgressCard">
|
||||
<h2 class="tp-matrix__title">{{ targetProgressCard.title }}</h2>
|
||||
<div class="tp-matrix" role="list">
|
||||
<div
|
||||
v-for="sec in targetProgressCard.sections"
|
||||
:key="'tm-' + sec.dept_id"
|
||||
class="tp-matrix-card"
|
||||
role="listitem"
|
||||
>
|
||||
<div class="tp-matrix-card__head">
|
||||
<div class="tp-matrix-card__titles">
|
||||
<span v-if="sec.parent_dept_name" class="tp-matrix-card__parent">{{
|
||||
sec.parent_dept_name
|
||||
}}</span>
|
||||
<span class="tp-matrix-card__name">{{ sec.dept_name }}目标</span>
|
||||
</div>
|
||||
<span class="tp-matrix-card__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
||||
</div>
|
||||
<table class="tp-matrix-card__table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">昨日诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.yesterday_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.douyin_amount !== null">
|
||||
<td class="tp-matrix-card__lbl">抖音总诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.douyin_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.tuifen_amount !== null">
|
||||
<td class="tp-matrix-card__lbl">推粉总诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.tuifen_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">总诊金</td>
|
||||
<td class="tp-matrix-card__num tp-matrix-card__num--hot">
|
||||
{{ formatInt(sec.total_amount) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">目标进度</td>
|
||||
<td class="tp-matrix-card__num">{{ formatMoney(sec.target_progress) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">完成进度</td>
|
||||
<td
|
||||
class="tp-matrix-card__num tp-matrix-card__num--hot"
|
||||
:class="achievementClass(sec.completion_rate)"
|
||||
>
|
||||
{{ formatAchievement(sec.completion_rate) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty
|
||||
v-else-if="!targetProgressLoading"
|
||||
class="tp-matrix-empty"
|
||||
description="暂无目标数据,请先配置当月部门目标或为部门设置诊金目标后刷新页面"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
@@ -936,7 +974,7 @@
|
||||
>
|
||||
<span class="yeji-order-drawer__stat-label">
|
||||
业务订单金额合计
|
||||
<span class="yeji-order-drawer__stat-note">(不含履约已取消,与上方业绩口径一致)</span>
|
||||
<span class="yeji-order-drawer__stat-note">(不含履约 4·9·10,与上方业绩口径一致)</span>
|
||||
</span>
|
||||
<span class="yeji-order-drawer__stat-value yeji-order-drawer__stat-value--money">
|
||||
¥{{ formatOrderDrawerMoney(orderDrawerTotalAmount) }}
|
||||
@@ -1047,6 +1085,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doctor_name" label="开方人" width="88" show-overflow-tooltip />
|
||||
<el-table-column prop="assistant_name" label="诊单医助" width="88" show-overflow-tooltip />
|
||||
<el-table-column prop="creator_name" label="创建人" width="88" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-empty v-if="!orderDrawerLoading && orderDrawerLists.length === 0" description="暂无业务订单" />
|
||||
@@ -1197,6 +1236,7 @@
|
||||
<el-table-column prop="patient_name" label="患者" min-width="92" show-overflow-tooltip />
|
||||
<el-table-column prop="patient_phone" label="手机" min-width="116" show-overflow-tooltip />
|
||||
<el-table-column prop="doctor_name" label="接诊医生" width="92" show-overflow-tooltip />
|
||||
<el-table-column prop="assistant_name" label="医助" width="88" show-overflow-tooltip />
|
||||
<el-table-column label="诊单ID" width="84" align="right">
|
||||
<template #default="{ row }">{{ formatInt(row.diagnosis_id ?? 0) }}</template>
|
||||
</el-table-column>
|
||||
@@ -1279,6 +1319,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import vCharts from 'vue-echarts'
|
||||
@@ -1623,7 +1664,7 @@ const appointmentLinesShowChannelColumn = computed(() =>
|
||||
)
|
||||
|
||||
/** 图表 / 数据表切换 */
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt'>('zyyt')
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'>('zyyt')
|
||||
|
||||
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
|
||||
const userStore = useUserStore()
|
||||
@@ -2040,7 +2081,9 @@ async function loadDoctorDailyStats() {
|
||||
if (res?.start_date && res?.end_date) {
|
||||
doctorDailyRange.value = { start: res.start_date, end: res.end_date }
|
||||
}
|
||||
} catch (_e) {
|
||||
} catch (e: unknown) {
|
||||
// 与业绩主接口并发时,同源请求键相同会触发 axios 重复请求取消;勿清空状态以免覆盖后到的成功结果
|
||||
if (axios.isCancel(e)) return
|
||||
doctorDailyRows.value = []
|
||||
doctorDailyTotal.value = {}
|
||||
doctorDailyRange.value = null
|
||||
@@ -2069,7 +2112,15 @@ const orderDrawerSplitPerfCache = ref<{
|
||||
} | null>(null)
|
||||
|
||||
type OrderDrawerFilter =
|
||||
| { mode: 'dept'; deptId: number; deptName: string; start: string; end: string }
|
||||
| {
|
||||
mode: 'dept'
|
||||
deptId: number
|
||||
deptName: string
|
||||
start: string
|
||||
end: string
|
||||
/** 当前业绩表各展示行 dept_id,用于侧栏与「接诊诊单」列对齐(创建人优先 + 最深展示行) */
|
||||
yejiTableRowDeptIds?: number[]
|
||||
}
|
||||
| {
|
||||
mode: 'assistant'
|
||||
assistantId: number
|
||||
@@ -2081,7 +2132,7 @@ type OrderDrawerFilter =
|
||||
}
|
||||
|
||||
const orderDrawerFilter = ref<OrderDrawerFilter | null>(null)
|
||||
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选、不含已取消口径一致) */
|
||||
/** 列表接口 extend,含 stats_order_amount_performance(与当前筛选一致:剔除履约 4/9/10) */
|
||||
const orderDrawerExtend = ref<Record<string, any> | null>(null)
|
||||
|
||||
const orderDrawerTotalAmount = computed(() => {
|
||||
@@ -2429,7 +2480,8 @@ async function loadLeaderboard(range: { start: string; end: string }) {
|
||||
range_note: res.range_note || '',
|
||||
leaderboards: res.leaderboards || [],
|
||||
}
|
||||
} catch (_e) {
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
leaderboardBlock.value = null
|
||||
} finally {
|
||||
leaderboardsLoading.value = false
|
||||
@@ -2484,8 +2536,10 @@ async function loadData() {
|
||||
}))
|
||||
const ymd = formatLocalYmd(new Date())
|
||||
await loadLeaderboard({ start: ymd, end: ymd })
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载失败')
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (canViewDoctorDailyStats.value) {
|
||||
@@ -2586,40 +2640,82 @@ function formatOrderDrawerTime(t: unknown): string {
|
||||
return s.length >= 16 ? s.slice(0, 16) : s
|
||||
}
|
||||
|
||||
async function fetchOrderDrawerPage() {
|
||||
/** 业务订单侧栏列表筛选(不含分页) */
|
||||
function buildOrderDrawerListParams(): Record<string, any> | null {
|
||||
const f = orderDrawerFilter.value
|
||||
if (!f) return
|
||||
if (!f) return null
|
||||
const params: Record<string, any> = {
|
||||
start_time: `${f.start} 00:00:00`,
|
||||
end_time: `${f.end} 23:59:59`,
|
||||
/** 与业绩列口径一致:不列出履约 4(已取消)/9(拒收)/10(退款) */
|
||||
exclude_fulfillment_cancelled: 1,
|
||||
/**
|
||||
* 业绩看板入口标志:后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后,
|
||||
* assistant_id / assistant_dept_id 一律按订单创建人收窄(与表格诊金/合计业绩/复诊同口径),
|
||||
* 而非默认的「创建人 ∪ 诊单医助」并集。同时启用 extend 字段(约诊数、合计业绩对照)。
|
||||
*/
|
||||
yeji_order_drawer: 1,
|
||||
}
|
||||
if (f.mode === 'dept') {
|
||||
params.assistant_dept_id = f.deptId
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
params.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
} else {
|
||||
params.assistant_id = f.assistantId
|
||||
if (f.erCenterRevisitSlot !== undefined) {
|
||||
params.yeji_er_center_revisit_only = 1
|
||||
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerVisible.value &&
|
||||
orderDrawerListTab.value === 'channel'
|
||||
) {
|
||||
params.channel_code = selectedChannel.value
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
async function hydrateOrderDrawerSplitPerfCache() {
|
||||
const base = buildOrderDrawerListParams()
|
||||
if (!base || !selectedChannel.value) return
|
||||
const params: Record<string, any> = {
|
||||
...base,
|
||||
channel_code: selectedChannel.value,
|
||||
page_no: 1,
|
||||
page_size: 1,
|
||||
}
|
||||
try {
|
||||
const res: any = await prescriptionOrderLists(params)
|
||||
const ex = res?.extend && typeof res.extend === 'object' ? res.extend : null
|
||||
if (ex && ex.stats_yeji_drawer_total_performance_amount != null) {
|
||||
orderDrawerSplitPerfCache.value = {
|
||||
totalAmount: Number(ex.stats_yeji_drawer_total_performance_amount),
|
||||
totalOrderCount: Number(ex.stats_yeji_drawer_total_performance_order_count ?? 0),
|
||||
channelAmount: Number(ex.stats_order_amount_performance ?? 0),
|
||||
channelOrderCount: Number(res?.count ?? 0),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 对照指标失败不打断列表
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrderDrawerPage() {
|
||||
const base = buildOrderDrawerListParams()
|
||||
if (!base) return
|
||||
if (selectedChannel.value && orderDrawerListTab.value === 'total') {
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
}
|
||||
orderDrawerLoading.value = true
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
...base,
|
||||
page_no: orderDrawerPage.value,
|
||||
page_size: orderDrawerPageSize.value,
|
||||
start_time: `${f.start} 00:00:00`,
|
||||
end_time: `${f.end} 23:59:59`,
|
||||
/** 与业绩列口径一致:不列出履约已取消(4) */
|
||||
exclude_fulfillment_cancelled: 1,
|
||||
/** 侧栏展示约诊数等 extend 字段 */
|
||||
yeji_order_drawer: 1,
|
||||
}
|
||||
if (f.mode === 'dept') {
|
||||
params.assistant_dept_id = f.deptId
|
||||
/** 与看板「接诊诊单」列一致:处方列表默认 assistant_dept_id(诊单医助或创建人∪子树),不用业绩摊行医助集合 */
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
params.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
} else {
|
||||
params.assistant_id = f.assistantId
|
||||
if (f.erCenterRevisitSlot !== undefined) {
|
||||
params.yeji_er_center_revisit_only = 1
|
||||
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerChannelSplitActive.value &&
|
||||
orderDrawerListTab.value === 'channel'
|
||||
) {
|
||||
params.channel_code = selectedChannel.value
|
||||
}
|
||||
const res: any = await prescriptionOrderLists(params)
|
||||
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
@@ -2640,8 +2736,17 @@ async function fetchOrderDrawerPage() {
|
||||
channelOrderCount: Number(res?.count ?? 0),
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerVisible.value &&
|
||||
orderDrawerListTab.value === 'total'
|
||||
) {
|
||||
await hydrateOrderDrawerSplitPerfCache()
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载业务订单失败')
|
||||
orderDrawerLists.value = []
|
||||
orderDrawerCount.value = 0
|
||||
orderDrawerExtend.value = null
|
||||
@@ -2650,8 +2755,9 @@ async function fetchOrderDrawerPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: 'total' | 'channel' }) {
|
||||
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||
orderDrawerListTab.value = channelSplit ? opts?.listTab ?? 'channel' : 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'dept',
|
||||
@@ -2659,14 +2765,79 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
deptName: row.dept_name,
|
||||
start: tb.start_date,
|
||||
end: tb.end_date,
|
||||
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
|
||||
}
|
||||
orderDrawerTitle.value = `业务订单 · ${row.dept_name}`
|
||||
orderDrawerHint.value = `创建时间:${tb.start_date} ~ ${tb.end_date} · 与「接诊诊单」同口径:诊单医助或订单创建人落在本部门(含下级)· 不含履约已取消`
|
||||
orderDrawerHint.value = `创建时间:${tb.start_date} ~ ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约 4/9/10`
|
||||
orderDrawerPage.value = 1
|
||||
orderDrawerVisible.value = true
|
||||
void fetchOrderDrawerPage()
|
||||
}
|
||||
|
||||
/** 顶部「展示部门」仅选中 1 个根节点时:合计行业绩对应该子树下的订单汇总 */
|
||||
function openOrderDrawerByYejiTableScope(
|
||||
tb: YejiTable,
|
||||
rootDeptId: number,
|
||||
listTab: 'total' | 'channel'
|
||||
) {
|
||||
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||
orderDrawerListTab.value = channelSplit ? listTab : 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'dept',
|
||||
deptId: rootDeptId,
|
||||
deptName: '当前展示合计',
|
||||
start: tb.start_date,
|
||||
end: tb.end_date,
|
||||
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
|
||||
}
|
||||
orderDrawerTitle.value = '业务订单 · 当前展示合计'
|
||||
orderDrawerHint.value = `创建时间:${tb.start_date} ~ ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约 4/9/10`
|
||||
orderDrawerPage.value = 1
|
||||
orderDrawerVisible.value = true
|
||||
void fetchOrderDrawerPage()
|
||||
}
|
||||
|
||||
function yejiPerfCellDrawerEligible(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel'): boolean {
|
||||
if (isUnassignedYejiRow(row) || row.dept_id <= 0) {
|
||||
return false
|
||||
}
|
||||
if (kind === 'channel' && !tb.channel_name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function yejiPerfTotalRowDrawerEligible(tb: YejiTable, kind: 'total' | 'channel'): boolean {
|
||||
if (selectedDeptIds.value.length !== 1) {
|
||||
return false
|
||||
}
|
||||
if (kind === 'channel' && !tb.channel_name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function onYejiPerformanceCellClick(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel') {
|
||||
if (!yejiPerfCellDrawerEligible(tb, row, kind)) {
|
||||
return
|
||||
}
|
||||
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||
openOrderDrawerByDept(tb, row, { listTab })
|
||||
}
|
||||
|
||||
function onYejiPerformanceTotalRowClick(tb: YejiTable, kind: 'total' | 'channel') {
|
||||
if (!yejiPerfTotalRowDrawerEligible(tb, kind)) {
|
||||
return
|
||||
}
|
||||
const rootDeptId = selectedDeptIds.value[0]
|
||||
if (!rootDeptId) {
|
||||
return
|
||||
}
|
||||
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||
openOrderDrawerByYejiTableScope(tb, rootDeptId, listTab)
|
||||
}
|
||||
|
||||
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
@@ -2678,7 +2849,7 @@ function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['row
|
||||
end,
|
||||
}
|
||||
orderDrawerTitle.value = `业务订单 · ${row.name}`
|
||||
orderDrawerHint.value = `创建时间:${start} ~ ${end} · 诊单医助 · 不含履约已取消`
|
||||
orderDrawerHint.value = `创建时间:${start} ~ ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约 4/9/10`
|
||||
orderDrawerPage.value = 1
|
||||
orderDrawerVisible.value = true
|
||||
void fetchOrderDrawerPage()
|
||||
@@ -2702,7 +2873,7 @@ function openOrderDrawerByAssistantErCenterRevisit(
|
||||
}
|
||||
const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
|
||||
orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
|
||||
orderDrawerHint.value = `创建时间:${start} ~ ${end} · 诊单医助 · 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
|
||||
orderDrawerHint.value = `创建时间:${start} ~ ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约 4/9/10`
|
||||
orderDrawerPage.value = 1
|
||||
orderDrawerVisible.value = true
|
||||
void fetchOrderDrawerPage()
|
||||
@@ -2815,8 +2986,10 @@ async function openYejiRevisitDeptBreakdown(tb: YejiTable, row: YejiRow, revisit
|
||||
const res: any = await yejiStatsRevisitBreakdown(p as any)
|
||||
revisitBreakdownRows.value = Array.isArray(res?.rows) ? res.rows : []
|
||||
revisitBreakdownNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载复诊拆解失败')
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载复诊拆解失败')
|
||||
revisitBreakdownRows.value = []
|
||||
revisitBreakdownNote.value = ''
|
||||
} finally {
|
||||
@@ -2855,8 +3028,10 @@ async function openUnassignedBreakdown(tb: YejiTable) {
|
||||
const res: any = await yejiStatsUnassignedBreakdown(p as any)
|
||||
unassignedDialogRows.value = Array.isArray(res?.rows) ? res.rows : []
|
||||
unassignedDialogNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载未归属拆解失败')
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载未归属拆解失败')
|
||||
unassignedDialogRows.value = []
|
||||
unassignedDialogNote.value = ''
|
||||
} finally {
|
||||
@@ -2897,8 +3072,10 @@ async function fetchLeadLinesPage() {
|
||||
leadLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
leadLinesCount.value = Number(res?.count ?? 0)
|
||||
leadLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载进线明细失败')
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载进线明细失败')
|
||||
leadLinesRows.value = []
|
||||
leadLinesCount.value = 0
|
||||
leadLinesApiNote.value = ''
|
||||
@@ -2970,8 +3147,10 @@ async function fetchAppointmentLinesPage() {
|
||||
appointmentLinesRows.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
appointmentLinesCount.value = Number(res?.count ?? 0)
|
||||
appointmentLinesApiNote.value = typeof res?.note === 'string' ? res.note : ''
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载挂号明细失败')
|
||||
} catch (e: unknown) {
|
||||
if (axios.isCancel(e)) return
|
||||
const any = e as any
|
||||
ElMessage.error(any?.msg || any?.message || '加载挂号明细失败')
|
||||
appointmentLinesRows.value = []
|
||||
appointmentLinesCount.value = 0
|
||||
appointmentLinesApiNote.value = ''
|
||||
@@ -3946,118 +4125,7 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 目标进度卡片 ── */
|
||||
.tp-wrap {
|
||||
min-height: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tp-card {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
border-radius: var(--yj-radius);
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
background: var(--yj-surface);
|
||||
border: 1px solid var(--yj-line);
|
||||
box-shadow: var(--yj-shadow-md);
|
||||
|
||||
&__title {
|
||||
background: var(--yj-surface-muted);
|
||||
color: var(--yj-ink);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 14px 20px;
|
||||
letter-spacing: 0.04em;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tp-section {
|
||||
min-width: 200px;
|
||||
flex: 1 1 200px;
|
||||
border-right: 1px solid var(--yj-line);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
background: var(--yj-brand-soft);
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__title-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__parent {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--yj-muted);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--yj-ink);
|
||||
}
|
||||
|
||||
&__target {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.tp-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.tp-lbl {
|
||||
padding: 8px 12px;
|
||||
color: var(--yj-muted);
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tp-val {
|
||||
padding: 8px 14px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
color: var(--yj-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--total {
|
||||
color: var(--yj-accent);
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 目标完成率色条:目标看板(卡片)Tab 复用 */
|
||||
.tp-good {
|
||||
color: #15803d !important;
|
||||
font-weight: 700;
|
||||
@@ -4071,6 +4139,126 @@ onMounted(async () => {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 目标看板卡片栅格(与顶部「甄养堂」条带数据同源,8 列铺满) */
|
||||
.tp-matrix-wrap {
|
||||
min-height: 120px;
|
||||
padding: 8px 4px 16px;
|
||||
}
|
||||
|
||||
.tp-matrix__title {
|
||||
margin: 0 0 18px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-ink);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.tp-matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
.tp-matrix {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.tp-matrix {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.tp-matrix-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--yj-line);
|
||||
border-radius: var(--yj-radius-sm);
|
||||
background: var(--yj-surface);
|
||||
box-shadow: var(--yj-shadow-sm);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--yj-brand-soft);
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__parent {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--yj-muted);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: var(--yj-ink);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
&__target {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
&__lbl {
|
||||
padding: 7px 10px;
|
||||
color: var(--yj-muted);
|
||||
font-size: 11px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__num {
|
||||
padding: 7px 10px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
color: var(--yj-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--hot {
|
||||
color: var(--yj-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tp-matrix-empty {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.leaderboard-sub {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -4640,6 +4828,10 @@ tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
|
||||
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||
}
|
||||
|
||||
tbody tr.total-row:hover .yeji-lead-cell--link {
|
||||
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||
}
|
||||
|
||||
.yeji-leadlines-dialog__pager {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
|
||||
@@ -22,16 +22,25 @@
|
||||
<el-select
|
||||
v-model="formData.media_channel_code"
|
||||
placeholder="请选择自媒体渠道"
|
||||
class="w-full"
|
||||
class="account-cost-channel-select w-full"
|
||||
filterable
|
||||
:disabled="mode === 'edit'"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in mediaChannelOptions"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
<el-option-group
|
||||
v-for="g in mediaChannelGroups"
|
||||
:key="g.group_name"
|
||||
:label="g.group_name"
|
||||
>
|
||||
<el-option
|
||||
v-for="ch in g.channels"
|
||||
:key="ch.channel_code"
|
||||
:label="ch.channel_name"
|
||||
:value="ch.channel_code"
|
||||
>
|
||||
<div class="channel-opt-row">
|
||||
<span class="opt-name">{{ ch.channel_name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="dept_id">
|
||||
@@ -78,9 +87,9 @@ import type { FormInstance } from 'element-plus'
|
||||
import { accountCostAdd, accountCostDetail, accountCostEdit } from '@/api/finance'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
|
||||
interface MediaChannelOption {
|
||||
code: string
|
||||
name: string
|
||||
interface MediaChannelGroup {
|
||||
group_name: string
|
||||
channels: { channel_code: string; channel_name: string }[]
|
||||
}
|
||||
|
||||
interface DeptOption {
|
||||
@@ -90,7 +99,7 @@ interface DeptOption {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
mediaChannelOptions: MediaChannelOption[]
|
||||
mediaChannelGroups: MediaChannelGroup[]
|
||||
deptOptions: DeptOption[]
|
||||
defaultMediaChannelCode: string
|
||||
}>()
|
||||
@@ -197,3 +206,21 @@ defineExpose({
|
||||
getDetail,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-cost-channel-select {
|
||||
:deep(.channel-opt-row) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
:deep(.opt-name) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,14 +27,25 @@
|
||||
placeholder="筛选全部渠道"
|
||||
clearable
|
||||
filterable
|
||||
class="w-[220px]"
|
||||
class="account-cost-channel-select w-[220px]"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in mediaChannelOptions"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option-group
|
||||
v-for="g in mediaChannelGroups"
|
||||
:key="g.group_name"
|
||||
:label="g.group_name"
|
||||
>
|
||||
<el-option
|
||||
v-for="ch in g.channels"
|
||||
:key="ch.channel_code"
|
||||
:label="ch.channel_name"
|
||||
:value="ch.channel_code"
|
||||
>
|
||||
<div class="channel-opt-row">
|
||||
<span class="opt-name">{{ ch.channel_name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
@@ -117,7 +128,7 @@
|
||||
<edit-popup
|
||||
v-if="showEdit"
|
||||
ref="editRef"
|
||||
:media-channel-options="mediaChannelOptions"
|
||||
:media-channel-groups="mediaChannelGroups"
|
||||
:dept-options="deptOptions"
|
||||
:default-media-channel-code="defaultMediaChannelCode"
|
||||
@success="getLists"
|
||||
@@ -133,6 +144,11 @@ import feedback from '@/utils/feedback'
|
||||
|
||||
import EditPopup from './edit.vue'
|
||||
|
||||
interface MediaChannelGroup {
|
||||
group_name: string
|
||||
channels: { channel_code: string; channel_name: string }[]
|
||||
}
|
||||
|
||||
interface MediaChannelOption {
|
||||
code: string
|
||||
name: string
|
||||
@@ -160,7 +176,7 @@ const { pager, getLists, resetPage } = usePaging({
|
||||
params: queryParams,
|
||||
})
|
||||
|
||||
const mediaChannelOptions = computed<MediaChannelOption[]>(() => {
|
||||
const mediaChannelOptionsFlat = computed<MediaChannelOption[]>(() => {
|
||||
const options = pager.extend.media_channel_options
|
||||
if (!Array.isArray(options)) return []
|
||||
|
||||
@@ -170,6 +186,26 @@ const mediaChannelOptions = computed<MediaChannelOption[]>(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const mediaChannelGroups = computed<MediaChannelGroup[]>(() => {
|
||||
const groups = pager.extend.media_channel_groups
|
||||
if (Array.isArray(groups) && groups.length > 0) {
|
||||
return groups as MediaChannelGroup[]
|
||||
}
|
||||
const flat = mediaChannelOptionsFlat.value
|
||||
if (!flat.length) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
group_name: '渠道',
|
||||
channels: flat.map(item => ({
|
||||
channel_code: item.code,
|
||||
channel_name: item.name,
|
||||
})),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const deptOptions = computed<DeptOption[]>(() => {
|
||||
const options = pager.extend.dept_options
|
||||
return Array.isArray(options) ? options : []
|
||||
@@ -213,3 +249,21 @@ const handleReset = () => {
|
||||
|
||||
getLists()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-cost-channel-select {
|
||||
:deep(.channel-opt-row) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
:deep(.opt-name) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -180,11 +180,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="订单状态" width="100">
|
||||
<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>
|
||||
|
||||
@@ -270,6 +271,14 @@
|
||||
>
|
||||
删除
|
||||
</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>
|
||||
@@ -779,7 +788,8 @@ import {
|
||||
orderActionLogs,
|
||||
orderActionLogStats,
|
||||
orderBatchAssignAssistant,
|
||||
orderSplit
|
||||
orderSplit,
|
||||
orderSetExempt
|
||||
} from '@/api/order'
|
||||
import { getAssistants } from '@/api/tcm'
|
||||
import { generateOrderQrcode } from '@/api/tcm'
|
||||
@@ -1494,6 +1504,19 @@ const handleDelete = async (row: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -49,15 +49,30 @@
|
||||
v-model="form.channel_source"
|
||||
placeholder="请选择渠道来源"
|
||||
class="channel-source-select"
|
||||
@change="onChannelSourceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.value"
|
||||
:key="String(item.value)"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="needsChannelSourceDetail"
|
||||
label="自媒体补充:"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="form.channel_source_detail"
|
||||
clearable
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入自媒体相关补充内容"
|
||||
class="channel-source-select"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 预约医生 -->
|
||||
<el-form-item label="预约医生:">
|
||||
<div class="doctor-list">
|
||||
@@ -233,9 +248,44 @@ const form = reactive({
|
||||
date: '',
|
||||
appointmentTime: '',
|
||||
remark: '',
|
||||
channel_source: '' as string
|
||||
channel_source: '' as string,
|
||||
channel_source_detail: '' as string
|
||||
})
|
||||
|
||||
/** 仅这些渠道字典 name 需填「自媒体补充」(与后台字典名称完全一致) */
|
||||
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
||||
'自媒体4H',
|
||||
'自媒体3Q',
|
||||
'自媒体3H',
|
||||
'自媒体2H',
|
||||
'自媒体2Q'
|
||||
])
|
||||
|
||||
function channelNameRequiresSelfMediaDetail(name: string) {
|
||||
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
||||
}
|
||||
|
||||
/** 当前选中渠道字典名称(用于判断是否需填「自媒体补充」) */
|
||||
const selectedChannelDictName = computed(() => {
|
||||
const v = form.channel_source
|
||||
if (v === '' || v == null) return ''
|
||||
const row = channelOptions.value.find((item: any) => String(item.value) === String(v))
|
||||
return row ? String(row.name ?? '').trim() : ''
|
||||
})
|
||||
|
||||
/** 仅白名单内字典项需填写补充说明 */
|
||||
const needsChannelSourceDetail = computed(() =>
|
||||
channelNameRequiresSelfMediaDetail(selectedChannelDictName.value)
|
||||
)
|
||||
|
||||
const onChannelSourceChange = (val: string) => {
|
||||
const row = channelOptions.value.find((item: any) => String(item.value) === String(val))
|
||||
const name = row ? String(row.name ?? '').trim() : ''
|
||||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||||
form.channel_source_detail = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
||||
const dateOptions = computed<DateOption[]>(() => {
|
||||
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
||||
@@ -321,7 +371,8 @@ const canSubmit = computed(() => {
|
||||
selectedDoctorId.value &&
|
||||
form.date &&
|
||||
form.appointmentTime &&
|
||||
!!form.channel_source
|
||||
!!form.channel_source &&
|
||||
(!needsChannelSourceDetail.value || form.channel_source_detail.trim() !== '')
|
||||
)
|
||||
})
|
||||
|
||||
@@ -344,6 +395,7 @@ const open = async (patient: any) => {
|
||||
form.appointmentTime = ''
|
||||
form.remark = ''
|
||||
form.channel_source = ''
|
||||
form.channel_source_detail = ''
|
||||
timeSlots.value = []
|
||||
doctorRosterDates.value = []
|
||||
lastVisit.value = ''
|
||||
@@ -412,7 +464,17 @@ const loadLastVisit = async () => {
|
||||
const loadChannelOptions = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
channelOptions.value = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
const rows = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
rows.sort((a: any, b: any) => {
|
||||
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||||
if (ds !== 0) return ds
|
||||
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||||
})
|
||||
// 统一为字符串,避免字典 value 为数字时 el-select 与表单校验不一致导致看似选了但实际未绑定
|
||||
channelOptions.value = rows.map((row: any) => ({
|
||||
...row,
|
||||
value: row?.value != null && row.value !== '' ? String(row.value) : ''
|
||||
}))
|
||||
} catch (e) {
|
||||
console.error('加载渠道来源失败:', e)
|
||||
channelOptions.value = []
|
||||
@@ -612,6 +674,10 @@ const handleConfirm = async () => {
|
||||
feedback.msgWarning('请选择渠道来源')
|
||||
return
|
||||
}
|
||||
if (needsChannelSourceDetail.value && !form.channel_source_detail.trim()) {
|
||||
feedback.msgWarning('请填写自媒体渠道补充信息')
|
||||
return
|
||||
}
|
||||
if (blockedByTodayDuplicate.value) {
|
||||
feedback.msgWarning(
|
||||
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
||||
@@ -631,7 +697,8 @@ const handleConfirm = async () => {
|
||||
appointment_time: form.appointmentTime,
|
||||
appointment_type: form.appointmentType,
|
||||
remark: form.remark,
|
||||
channel_source: form.channel_source
|
||||
channel_source: String(form.channel_source ?? ''),
|
||||
channel_source_detail: form.channel_source_detail.trim()
|
||||
}
|
||||
|
||||
console.log('提交预约参数:', params)
|
||||
|
||||
@@ -105,7 +105,9 @@ function channelLabel(row: Record<string, any>) {
|
||||
const raw = row.channel_source ?? row.channels ?? ''
|
||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||
const key = String(raw)
|
||||
return channelMap.value[key] || key
|
||||
const base = channelMap.value[key] || key
|
||||
const extra = String(row.channel_source_detail ?? '').trim()
|
||||
return extra !== '' ? `${base}(${extra})` : base
|
||||
}
|
||||
|
||||
function sortRows(list: any[]) {
|
||||
@@ -141,6 +143,8 @@ const load = async () => {
|
||||
await loadChannels()
|
||||
const res = await appointmentLists({
|
||||
patient_id: props.diagnosisId,
|
||||
/** 诊单维度拉挂号:后端豁免医生/医助与数据范围收窄 */
|
||||
diag_scope_relax: 1,
|
||||
page_no: 1,
|
||||
page_size: 500
|
||||
})
|
||||
|
||||
@@ -683,7 +683,7 @@ const bloodTrendOption = computed(() => ({
|
||||
name: '空腹血糖',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: false,
|
||||
connectNulls: true,
|
||||
data: bloodTrendSeries.value.fasting,
|
||||
symbolSize: 7,
|
||||
itemStyle: {
|
||||
@@ -698,7 +698,7 @@ const bloodTrendOption = computed(() => ({
|
||||
name: '餐后血糖',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
connectNulls: false,
|
||||
connectNulls: true,
|
||||
data: bloodTrendSeries.value.postprandial,
|
||||
symbolSize: 7,
|
||||
itemStyle: {
|
||||
@@ -719,7 +719,8 @@ function formatColumnDate(date: string) {
|
||||
function parseTrendNumber(value: unknown): number | null {
|
||||
if (value === '' || value == null) return null
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
if (!Number.isFinite(parsed) || parsed === 0) return null
|
||||
return parsed
|
||||
}
|
||||
|
||||
function getCell(metric: string, date: string): { value: string; isHigh: boolean; hasRecord: boolean } {
|
||||
|
||||
@@ -39,19 +39,315 @@
|
||||
{{ formatTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-perms="['tcm.prescriptionOrder/detail']"
|
||||
type="primary"
|
||||
link
|
||||
@click="openDetail(row.id)"
|
||||
>查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex justify-end mt-3">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 业务订单详情抽屉(只读,与 order_list.vue 详情保持一致) -->
|
||||
<el-drawer
|
||||
v-model="detailVisible"
|
||||
size="80%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="font-semibold text-lg">业务订单详情</span>
|
||||
<el-tag v-if="detailData?.order_no" type="primary" effect="plain" round class="ml-3">{{ detailData.order_no }}</el-tag>
|
||||
<el-tag v-if="detailData?.gancao_reciperl_order_no" type="success" effect="plain" round class="ml-2">甘草 {{ detailData.gancao_reciperl_order_no }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-skeleton v-if="detailLoading" :rows="12" animated class="px-6 py-4" />
|
||||
<div v-else-if="detailData" class="px-6 pb-6 overflow-y-auto">
|
||||
|
||||
<!-- 顶部数据概览 -->
|
||||
<el-row :gutter="16" class="mb-5">
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">总金额</div>
|
||||
<div class="text-red-500 font-bold text-xl">¥{{ detailData.amount }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">已付总额</div>
|
||||
<div class="text-green-600 font-bold text-xl">¥{{ detailData.linked_pay_paid_total || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">需代收</div>
|
||||
<div class="font-bold text-xl" :class="detailAgencyToCollect > 0 ? 'text-amber-600' : 'text-gray-500'">¥{{ formatMoney(detailAgencyToCollect) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-card shadow="never" class="border border-gray-100 bg-gray-50/50">
|
||||
<div class="text-gray-500 text-xs mb-1">已付笔数</div>
|
||||
<div class="text-primary font-bold text-xl">{{ detailLinkedPayOrders.length }} 笔</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<!-- 左侧:处方单(不含测试价格、改患者信息、药材明细) -->
|
||||
<el-card shadow="never" class="border-gray-100 h-full relative overflow-hidden">
|
||||
<div v-if="detailData.prescription_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
|
||||
<div v-if="detailData.prescription_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-[15px]">处方详情</span>
|
||||
<span v-if="detailData.doctor_name" class="text-xs text-gray-500">
|
||||
开方人:<span class="text-primary font-medium">{{ detailData.doctor_name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">ID: {{ detailData.prescription_id || '—' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="detailPrescription">
|
||||
<el-descriptions :column="2" border size="small" class="bg-white">
|
||||
<el-descriptions-item label="处方编号" :span="2">{{ detailPrescription.sn || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="患者">{{ detailPrescription.patient_name || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别 / 年龄">{{ detailPrescription.gender_desc || '—' }} · {{ detailPrescription.age ?? '—' }}岁</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">
|
||||
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold' : ''">{{ detailPrescription.phone || '—' }}</span>
|
||||
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与订单手机不一致</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处方日期">{{ detailPrescription.prescription_date || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医师">{{ detailPrescription.doctor_name || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ detailPrescription.prescription_type || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="临床诊断" :span="2">{{ detailPrescription.clinical_diagnosis || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="剂数 / 用法" :span="2">
|
||||
{{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} ·
|
||||
{{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处方金额">
|
||||
<span class="text-red-500 font-medium">¥{{ detailPrescription.amount ?? '—' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消费者审核">
|
||||
<el-tag :type="consumerRxAuditTag(detailPrescription.audit_status)" size="small">{{ consumerRxAuditText(detailPrescription.audit_status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用量">
|
||||
<template v-if="detailPrescription.dosage_amount">
|
||||
{{ detailPrescription.dosage_amount }}{{ detailPrescription.dosage_unit || 'g' }}
|
||||
<template v-if="detailPrescription.prescription_type === '浓缩水丸'">
|
||||
· {{ Number(detailPrescription.dosage_bag_count) > 0 ? Number(detailPrescription.dosage_bag_count) : 1 }}袋
|
||||
</template>
|
||||
<span v-if="detailPrescription.prescription_type === '饮片' && detailPrescription.need_decoction !== null" class="ml-2 text-gray-500">
|
||||
({{ detailPrescription.need_decoction ? '代煎' : '不代煎' }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>—</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服用方式">
|
||||
<div class="flex flex-col gap-1.5 text-sm leading-relaxed">
|
||||
<div>
|
||||
<span class="text-gray-500">每天次数:</span>
|
||||
{{ detailPrescription.times_per_day ? detailPrescription.times_per_day + ' 次' : '—' }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">处方开立:</span>
|
||||
{{ detailPrescription.usage_days != null && detailPrescription.usage_days !== '' ? detailPrescription.usage_days + ' 天' : '—' }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">订单设置:</span>
|
||||
{{ detailData.medication_days != null && String(detailData.medication_days).trim() !== '' ? detailData.medication_days + ' 天' : '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="Number(detailPrescription.void_status) === 1 ? 'danger' : 'success'" size="small">{{ Number(detailPrescription.void_status) === 1 ? '已作废' : '正常' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<el-empty v-else description="无处方数据" :image-size="72" />
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:未关联 + 已关联支付单 -->
|
||||
<div class="flex flex-col gap-4 h-full">
|
||||
<el-card shadow="never" class="border-gray-100">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-[15px]">未关联的收款记录</span>
|
||||
<span class="text-xs text-gray-400">本诊单下未被占用的支付单</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-if="detailUnlinkedPayOrders.length" :data="detailUnlinkedPayOrders" size="small" border stripe max-height="280">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="86" align="right">
|
||||
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="无未关联的收款单" :image-size="60" />
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="border-gray-100 relative overflow-hidden">
|
||||
<div v-if="detailData.payment_slip_audit_status === 1" class="audit-stamp stamp-pass"><div class="stamp-inner">审核通过</div></div>
|
||||
<div v-if="detailData.payment_slip_audit_status === 2" class="audit-stamp stamp-reject"><div class="stamp-inner">已驳回</div></div>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-medium text-[15px]">关联收款记录</span>
|
||||
<span v-if="detailData.creator_name" class="text-xs text-gray-500">创建人:<span class="text-primary font-medium">{{ detailData.creator_name }}</span></span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table v-if="detailLinkedPayOrders.length" :data="detailLinkedPayOrders" size="small" border stripe max-height="280">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="order_no" label="单号" min-width="135" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="90">
|
||||
<template #default="{ row }">{{ row.order_type_desc || feeTypeText(row.order_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="86" align="right">
|
||||
<template #default="{ row }"><span class="text-red-500">¥{{ row.amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payOrderStatusTag(row.status)" size="small">{{ row.status_desc || orderStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="creator_name" label="创建人" width="80" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" min-width="136">
|
||||
<template #default="{ row }">{{ formatOrderTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="未关联收款单" :image-size="60" />
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 履约与收货信息 -->
|
||||
<el-card shadow="never" class="border-gray-100 mb-4">
|
||||
<template #header><span class="font-medium text-[15px]">履约与收货信息</span></template>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="诊单ID" :span="3"><span class="font-mono">{{ detailData.diagnosis_id }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(detailData.create_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务订单创建人" :span="2">
|
||||
<div v-if="detailData.creator_name" class="text-[13px] space-y-1">
|
||||
<div><span class="font-medium text-gray-900">{{ detailData.creator_name || '—' }}</span></div>
|
||||
<div v-if="detailOrderCreatorMetaLine" class="text-gray-600 text-xs">{{ detailOrderCreatorMetaLine }}</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人所属部门" :span="3">
|
||||
<div v-if="detailOrderCreatorDeptBreadcrumbs.length" class="space-y-1.5">
|
||||
<div v-for="(segments, idx) in detailOrderCreatorDeptBreadcrumbs" :key="idx">
|
||||
<el-breadcrumb v-if="segments.length" class="text-[13px]" separator="/">
|
||||
<el-breadcrumb-item v-for="(name, j) in segments" :key="j"><span class="text-gray-800">{{ name }}</span></el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货人">{{ detailData.recipient_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收货手机">
|
||||
<span :class="detailRecipientRxPhoneMismatch ? 'text-red-600 font-bold font-mono' : 'font-mono'">{{ detailData.recipient_phone || '—' }}</span>
|
||||
<el-tag v-if="detailRecipientRxPhoneMismatch" type="danger" size="small" effect="dark" class="ml-2">与处方手机不一致</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址">{{ detailFullAddress }}</el-descriptions-item>
|
||||
<el-descriptions-item label="复诊">{{ detailData.is_follow_up ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用药疗程">{{ detailData.medication_days ? detailData.medication_days + ' 天' : '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上次医护">{{ detailData.prev_staff || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务渠道">{{ detailData.service_channel || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务套餐">{{ formatServicePackage(detailData.service_package) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="费用类别">{{ feeTypeText(detailData.fee_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递单号">{{ detailData.tracking_number || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="快递公司">{{ expressCompanyLabel(detailData.express_company) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-perms="['tcm.prescriptionOrder/editRemarkExtra']" label="药房备注" :span="3">{{ detailData.remark_extra || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医助备注" :span="3">{{ detailData.remark_assistant || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处方审核意见" :span="3">{{ detailData.prescription_audit_remark || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付审核意见" :span="3">{{ detailData.payment_slip_audit_remark || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 物流轨迹(只读) -->
|
||||
<el-card v-if="String(detailData.tracking_number || '').trim()" shadow="never" class="border-gray-100 mb-4">
|
||||
<template #header><span class="font-medium text-[15px]">物流轨迹</span></template>
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<el-input v-model="logisticsTracePhoneTail" clearable maxlength="20" placeholder="收件手机" class="w-[180px]" @keyup.enter="fetchLogisticsTrace" />
|
||||
<el-select v-model="detailLogisticsExpress" placeholder="承运商" style="width: 140px">
|
||||
<el-option label="自动识别" value="auto" />
|
||||
<el-option label="顺丰速运" value="sf" />
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-button v-perms="['tcm.prescriptionOrder/logisticsTrace']" type="primary" plain size="default" :loading="logisticsTraceLoading" @click="fetchLogisticsTrace">刷新轨迹</el-button>
|
||||
</div>
|
||||
<div v-if="logisticsTraceLoading && !logisticsTracePayload" class="py-6 text-center text-sm text-gray-500">正在加载物流轨迹...</div>
|
||||
<div v-else-if="logisticsTracePayload" class="bg-gray-50/50 p-4 rounded-md">
|
||||
<div v-if="logisticsTracePayload.state_text" class="text-sm mb-4 font-medium flex items-center gap-2">
|
||||
<span class="text-gray-800">状态:{{ logisticsTracePayload.state_text }}</span>
|
||||
<el-tag v-if="logisticsTracePayload.carrier_label" size="small" type="info">{{ logisticsTracePayload.carrier_label }}</el-tag>
|
||||
</div>
|
||||
<el-timeline v-if="logisticsTraceList.length" class="mt-2 pl-2">
|
||||
<el-timeline-item v-for="(t, idx) in logisticsTraceList" :key="idx" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="t.time" placement="top">
|
||||
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ t.context }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无轨迹节点记录" :image-size="64" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<el-card v-perms="['tcm.prescriptionOrder/logs']" shadow="never" class="border-gray-100">
|
||||
<template #header><span class="font-medium text-[15px]">操作日志</span></template>
|
||||
<el-timeline v-if="detailLogs.length" class="mt-2 pl-2">
|
||||
<el-timeline-item v-for="(log, idx) in detailLogs" :key="log.id" :type="idx === 0 ? 'primary' : 'info'" :hollow="idx !== 0" :timestamp="formatTime(log.create_time)" placement="top">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-medium text-[13px]">{{ log.admin_name }}</span>
|
||||
<el-tag size="small" type="info">{{ logActionText(log.action) }}</el-tag>
|
||||
</div>
|
||||
<span :class="idx === 0 ? 'text-gray-800 font-medium' : 'text-gray-500'">{{ log.summary }}</span>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无操作日志" :image-size="64" />
|
||||
</el-card>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, watch, ref, onMounted } from 'vue'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import { prescriptionOrderLists } from '@/api/tcm'
|
||||
import {
|
||||
prescriptionOrderLists,
|
||||
prescriptionOrderDetail,
|
||||
prescriptionOrderLogs,
|
||||
prescriptionOrderLogisticsTrace,
|
||||
prescriptionOrderPaidPayOrders
|
||||
} from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
@@ -83,11 +379,171 @@ const buildParams = () => {
|
||||
queryParams.scene = 'diagnosis_edit'
|
||||
}
|
||||
|
||||
// ─── 详情抽屉 ───
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<any>(null)
|
||||
const detailLogs = ref<any[]>([])
|
||||
const detailUnlinkedPayOrders = ref<any[]>([])
|
||||
|
||||
const detailPrescription = computed(() => {
|
||||
const p = detailData.value?.prescription
|
||||
return p && typeof p === 'object' ? p : null
|
||||
})
|
||||
|
||||
const detailLinkedPayOrders = computed(() => {
|
||||
const arr = detailData.value?.linked_pay_orders
|
||||
return Array.isArray(arr) ? arr : []
|
||||
})
|
||||
|
||||
const detailAgencyToCollect = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return 0
|
||||
const total = Number(d.amount) || 0
|
||||
const paid = Number(d.linked_pay_paid_total) || 0
|
||||
return Math.round((total - paid) * 100) / 100
|
||||
})
|
||||
|
||||
const detailRecipientRxPhoneMismatch = computed(() => {
|
||||
const rx = normalizeBizPhone(detailPrescription.value?.phone)
|
||||
const biz = normalizeBizPhone(detailData.value?.recipient_phone)
|
||||
if (!rx || !biz) return false
|
||||
return rx !== biz
|
||||
})
|
||||
|
||||
const detailFullAddress = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return '—'
|
||||
const parts = []
|
||||
if (d.shipping_province) parts.push(d.shipping_province)
|
||||
if (d.shipping_city) parts.push(d.shipping_city)
|
||||
if (d.shipping_district) parts.push(d.shipping_district)
|
||||
if (d.shipping_address) parts.push(d.shipping_address)
|
||||
return parts.length > 0 ? parts.join(' ') : '—'
|
||||
})
|
||||
|
||||
const detailOrderCreatorMetaLine = computed(() => {
|
||||
const d = detailData.value
|
||||
if (!d) return ''
|
||||
const account = String(d.creator_account ?? '').trim()
|
||||
const mobile = String(d.creator_mobile ?? '').trim()
|
||||
const parts: string[] = []
|
||||
if (account) parts.push(`登录账号:${account}`)
|
||||
if (mobile) parts.push(`手机:${mobile}`)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const detailOrderCreatorDeptBreadcrumbs = computed(() => {
|
||||
const raw = String(detailData.value?.order_creator_dept_path ?? detailData.value?.diagnosis_creator_dept_path ?? '').trim()
|
||||
if (!raw) return [] as string[][]
|
||||
return raw.split(';').map((s) => s.trim()).filter(Boolean)
|
||||
.map((p) => p.includes(' / ') ? p.split(' / ').map((x) => x.trim()).filter(Boolean) : p.split('/').map((x) => x.trim()).filter(Boolean))
|
||||
.filter((segs) => segs.length > 0)
|
||||
})
|
||||
|
||||
// ─── 物流轨迹 ───
|
||||
const detailLogisticsExpress = ref<string>('auto')
|
||||
const logisticsTraceLoading = ref(false)
|
||||
const logisticsTracePayload = ref<Record<string, any> | null>(null)
|
||||
const logisticsTracePhoneTail = ref('')
|
||||
|
||||
const logisticsTraceList = computed(() => {
|
||||
const p = logisticsTracePayload.value
|
||||
if (!p || !Array.isArray(p.traces)) return []
|
||||
return p.traces as Array<{ time: string; context: string }>
|
||||
})
|
||||
|
||||
async function fetchLogisticsTrace() {
|
||||
const id = Number(detailData.value?.id)
|
||||
if (!id) return
|
||||
logisticsTraceLoading.value = true
|
||||
try {
|
||||
const digits = String(logisticsTracePhoneTail.value || '').replace(/\D/g, '')
|
||||
const params: { id: number; express_company?: string; phone_tail?: string } = {
|
||||
id,
|
||||
express_company: detailLogisticsExpress.value
|
||||
}
|
||||
if (digits.length >= 4) params.phone_tail = digits
|
||||
const res: any = await prescriptionOrderLogisticsTrace(params)
|
||||
logisticsTracePayload.value = (res?.data ?? res) as Record<string, any>
|
||||
} catch {
|
||||
logisticsTracePayload.value = null
|
||||
} finally {
|
||||
logisticsTraceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 打开详情 ───
|
||||
async function openDetail(id: number) {
|
||||
detailData.value = null
|
||||
detailUnlinkedPayOrders.value = []
|
||||
logisticsTracePayload.value = null
|
||||
detailLogisticsExpress.value = 'auto'
|
||||
logisticsTracePhoneTail.value = ''
|
||||
detailLogs.value = []
|
||||
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res: any = await prescriptionOrderDetail({ id })
|
||||
const d = res?.data ?? res ?? null
|
||||
detailData.value = d
|
||||
if (d) {
|
||||
detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto'
|
||||
const dig = String(d.recipient_phone || '').replace(/\D/g, '')
|
||||
logisticsTracePhoneTail.value = dig.length >= 4 ? dig : ''
|
||||
if (String(d.tracking_number || '').trim()) fetchLogisticsTrace()
|
||||
fetchLogs(id)
|
||||
if (d.diagnosis_id) {
|
||||
loadDetailUnlinkedPayOrders(d.diagnosis_id, id, d.pay_order_ids || [])
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '加载详情失败')
|
||||
detailVisible.value = false
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) {
|
||||
if (!diagnosisId) { detailUnlinkedPayOrders.value = []; return }
|
||||
try {
|
||||
const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: diagnosisId, prescription_order_id: prescriptionOrderId })
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
detailUnlinkedPayOrders.value = Array.isArray(lists) ? lists.filter((item: any) => !linkedIds.includes(item.id)) : []
|
||||
} catch { detailUnlinkedPayOrders.value = [] }
|
||||
}
|
||||
|
||||
function canViewPrescriptionOrderLogs() {
|
||||
const p = userStore.perms || []
|
||||
return p.includes('*') || p.includes('tcm.prescriptionOrder/logs')
|
||||
}
|
||||
|
||||
async function fetchLogs(id: number) {
|
||||
if (!canViewPrescriptionOrderLogs()) { detailLogs.value = []; return }
|
||||
try {
|
||||
const res: any = await prescriptionOrderLogs({ id })
|
||||
detailLogs.value = res?.data ?? res ?? []
|
||||
} catch { detailLogs.value = [] }
|
||||
}
|
||||
|
||||
// ─── 工具函数 ───
|
||||
function normalizeBizPhone(v: unknown): string {
|
||||
if (v === null || v === undefined) return ''
|
||||
return String(v).replace(/\s/g, '').trim()
|
||||
}
|
||||
|
||||
const formatAmount = (value: unknown) => {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
function formatMoney(v: unknown) {
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
const formatTime = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (typeof value === 'number' && value > 1e9 && value < 1e11) {
|
||||
@@ -98,21 +554,86 @@ const formatTime = (value: unknown) => {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const fulfillmentText = (s: number | undefined) => {
|
||||
const m: Record<number, string> = {
|
||||
1: '待双审通过',
|
||||
2: '待发货',
|
||||
3: '已完成',
|
||||
4: '已取消',
|
||||
5: '已发货',
|
||||
6: '已签收',
|
||||
7: '进行中',
|
||||
8: '暂不制药',
|
||||
9: '拒收',
|
||||
10: '退款',
|
||||
11: '保留药方',
|
||||
12: '制药缓发'
|
||||
function formatOrderTime(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
if (typeof v === 'string' && String(v).includes('-')) return String(v)
|
||||
return formatTime(v)
|
||||
}
|
||||
|
||||
function feeTypeText(t: number | undefined) {
|
||||
const m: Record<number, string> = { 1: '药费', 2: '诊费', 3: '药费+诊费', 4: '其他' }
|
||||
return m[Number(t)] ?? '—'
|
||||
}
|
||||
|
||||
// 服务套餐字典选项(与 order_list.vue 数据源一致:dict 类型 server_order)
|
||||
const servicePackageOptions = ref<Array<{ name: string; value: string }>>([])
|
||||
|
||||
async function loadServicePackageOptions() {
|
||||
try {
|
||||
const data: any = await getDictData({ type: 'server_order' })
|
||||
const list = (data?.server_order || []) as Array<{ name: string; value: string; status?: number }>
|
||||
servicePackageOptions.value = list.filter((item) => item.status !== 0)
|
||||
} catch {
|
||||
servicePackageOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function formatServicePackage(value: unknown): string {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
let packages: string[] = []
|
||||
if (Array.isArray(value)) {
|
||||
packages = value.map((v) => String(v)).filter((v) => v !== '')
|
||||
} else if (typeof value === 'string') {
|
||||
packages = value.split(',').map((v) => v.trim()).filter((v) => v !== '')
|
||||
}
|
||||
if (packages.length === 0) return '—'
|
||||
const names = packages.map((val) => {
|
||||
const opt = servicePackageOptions.value.find((o) => o.value === val)
|
||||
return opt ? opt.name : val
|
||||
})
|
||||
return names.join('、')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServicePackageOptions()
|
||||
})
|
||||
|
||||
function expressCompanyLabel(v: unknown) {
|
||||
const s = String(v || '').toLowerCase()
|
||||
if (s === 'sf') return '顺丰速运'
|
||||
if (s === 'jd') return '京东快递'
|
||||
if (s === 'jt' || s === 'jtexpress') return '极兔速递'
|
||||
return '自动识别'
|
||||
}
|
||||
|
||||
function consumerRxAuditText(s: number | undefined) {
|
||||
const n = Number(s)
|
||||
if (n === 1) return '已通过'
|
||||
if (n === 2) return '已驳回'
|
||||
return '待审核'
|
||||
}
|
||||
|
||||
function consumerRxAuditTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 1) return 'success'
|
||||
if (n === 2) return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function payOrderStatusTag(s: number | undefined): 'success' | 'warning' | 'danger' | 'info' {
|
||||
const n = Number(s)
|
||||
if (n === 2) return 'success'
|
||||
if (n === 1) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function orderStatusText(s: number | undefined) {
|
||||
const m: Record<number, string> = { 0: '待支付', 1: '待支付', 2: '已支付', 3: '已取消' }
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
const fulfillmentText = (s: number | undefined) => {
|
||||
const m: Record<number, string> = { 1: '待双审通过', 2: '待发货', 3: '已完成', 4: '已取消', 5: '已发货', 6: '已签收', 7: '进行中', 8: '暂不制药', 9: '拒收', 10: '退款', 11: '保留药方', 12: '制药缓发' }
|
||||
return m[Number(s)] ?? '—'
|
||||
}
|
||||
|
||||
@@ -126,14 +647,22 @@ const fulfillmentTagType = (s: number | undefined): 'success' | 'warning' | 'dan
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function logActionText(act: string) {
|
||||
const m: Record<string, string> = {
|
||||
create: '创建', edit: '编辑', audit_rx_approve: '处方审核', audit_rx_reject: '处方审核',
|
||||
audit_pay_approve: '支付审核', audit_pay_reject: '支付审核', fill_tracking: '填快递单',
|
||||
ship: '确认发货', withdraw: '撤销', link_pay_order: '关联支付单',
|
||||
revoke_rx_audit: '撤回处方审核', revoke_pay_audit: '撤回支付审核',
|
||||
gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息',
|
||||
update_amount: '修改订单金额', complete: '完成订单'
|
||||
}
|
||||
return m[act] || act
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.diagnosisId, patientIdNum.value] as const,
|
||||
() => {
|
||||
if (!patientIdAvailable.value) {
|
||||
pager.lists = []
|
||||
pager.count = 0
|
||||
return
|
||||
}
|
||||
if (!patientIdAvailable.value) { pager.lists = []; pager.count = 0; return }
|
||||
buildParams()
|
||||
resetPage()
|
||||
},
|
||||
@@ -147,8 +676,25 @@ defineExpose({ refresh: () => getLists() })
|
||||
.patient-order-list {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.po-empty-tip {
|
||||
padding: 24px 0;
|
||||
}
|
||||
.audit-stamp {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
transform: rotate(15deg);
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.stamp-inner {
|
||||
border: 3px solid currentColor;
|
||||
border-radius: 8px;
|
||||
padding: 4px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stamp-pass { color: #22c55e; }
|
||||
.stamp-reject { color: #ef4444; }
|
||||
</style>
|
||||
|
||||
@@ -151,12 +151,14 @@
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column label="ID" min-width="50">
|
||||
<el-table-column label="ID" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<div class="patient-cell">
|
||||
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
|
||||
<div class="patient-info">
|
||||
<div class="patient-name">{{ row.id }}</div>
|
||||
<div class="patient-name">
|
||||
{{ row.id }}
|
||||
<el-tag v-if="row.assign_read_at === null && row.assistant_id > 0" type="danger" size="small" effect="dark" class="ml-1">NEW</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -255,7 +257,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="row.last_blood_record_at"
|
||||
:content="`最近一次血糖打卡:${row.last_blood_record_at}`"
|
||||
:content="`最近一次健康打卡:${row.last_blood_record_at}`"
|
||||
placement="top"
|
||||
>
|
||||
<span class="unserved-days" :class="unservedDaysClass(row.unserved_days)">
|
||||
@@ -795,6 +797,32 @@ function buildTcmDiagnosisListRequestPayload(req: Record<string, unknown>): Reco
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 待分配「订单月份」:未选月份时,未进入 Tab 前默认当月(与 handlePendingAssignTabClick 一致);
|
||||
* 在 Tab 内清空月份表示不限月份。
|
||||
*/
|
||||
function resolvePendingAssignOrderMonthForRequest(): string {
|
||||
const trimmed = String(formData.pending_assign_order_month ?? '').trim()
|
||||
if (trimmed !== '') return trimmed
|
||||
if (formData.pending_assign === '1') return ''
|
||||
return dayjs().format('YYYY-MM')
|
||||
}
|
||||
|
||||
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
|
||||
function buildPendingAssignCountPayload(): Record<string, unknown> {
|
||||
return buildTcmDiagnosisListRequestPayload({
|
||||
...formData,
|
||||
page_no: 1,
|
||||
page_size: 1,
|
||||
pending_assign: 1,
|
||||
appointment_date: '',
|
||||
has_appointment: '',
|
||||
pending_booking: '',
|
||||
completed_appointment: '',
|
||||
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
|
||||
} as Record<string, unknown>) as Record<string, unknown>
|
||||
}
|
||||
|
||||
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
|
||||
tcmDiagnosisLists(buildTcmDiagnosisListRequestPayload(req) as any)
|
||||
|
||||
@@ -923,16 +951,7 @@ const fetchDateCounts = async () => {
|
||||
tcmDiagnosisLists({ page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists(
|
||||
buildTcmDiagnosisListRequestPayload({
|
||||
pending_assign: 1,
|
||||
page_no: 1,
|
||||
page_size: 1,
|
||||
pending_assign_order_month: formData.pending_assign_order_month,
|
||||
pending_assign_keyword: formData.pending_assign_keyword,
|
||||
keyword: formData.keyword
|
||||
}) as any
|
||||
)
|
||||
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
|
||||
])
|
||||
dateCounts.value = {
|
||||
[yesterdayStr.value]: yesterday?.count ?? 0,
|
||||
@@ -1352,7 +1371,7 @@ const handleSingleAssign = (row: any) => {
|
||||
currentRow.value = row
|
||||
batchAssignIds.value = []
|
||||
// 确保类型一致,转换为数字
|
||||
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
|
||||
assignForm.value.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
</div>
|
||||
<div class="dh5-card-sub">
|
||||
#{{ row.id }} · {{ row.diagnosis_date_text || formatDateTime(row.create_time) }}
|
||||
<span v-if="row.assign_read_at === null && row.assistant_id > 0" class="dh5-tag-new">NEW</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dh5-card-tags">
|
||||
@@ -1672,6 +1673,18 @@ $dh5-card-bg: #ffffff;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dh5-tag-new {
|
||||
display: inline-block;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dh5-meta-gender,
|
||||
.dh5-meta-age {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
</span>
|
||||
<el-tooltip
|
||||
v-if="lastBloodAt"
|
||||
:content="`最近一次血糖打卡:${lastBloodAt}`"
|
||||
:content="`最近一次健康打卡:${lastBloodAt}`"
|
||||
placement="bottom"
|
||||
>
|
||||
<span :class="['unserved-badge', unservedDaysClass(unservedDays)]">
|
||||
未服务 {{ unservedDays ?? '—' }} 天
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="unserved-badge unserved-muted">从未打卡血糖</span>
|
||||
<span v-else class="unserved-badge unserved-muted">从未打卡</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,12 +97,14 @@
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column label="ID" min-width="50">
|
||||
<el-table-column label="ID" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<div class="patient-cell">
|
||||
<!-- <div class="patient-avatar">{{ (row.patient_name || '患').charAt(0) }}</div> -->
|
||||
<div class="patient-info">
|
||||
<div class="patient-name">{{ row.id }}</div>
|
||||
<div class="patient-name">
|
||||
{{ row.id }}
|
||||
<el-tag v-if="row.assign_read_at === null && row.assistant_id > 0" type="danger" size="small" effect="dark" class="ml-1">NEW</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1088,7 +1090,7 @@ const handleSingleAssign = (row: any) => {
|
||||
assignMode.value = 'single'
|
||||
currentRow.value = row
|
||||
// 确保类型一致,转换为数字
|
||||
assignForm.value.assistant_id = row.assistant_id ? Number(row.assistant_id) : null
|
||||
assignForm.value.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
|
||||
assignDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1 @@
|
||||
open_basedir=/www/wwwroot/zyt/server/:/tmp/
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+34
@@ -64,6 +64,40 @@ class AppointmentController extends BaseAdminController
|
||||
return $this->dataLists(new AppointmentLists());
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台编辑挂号记录(消费者处方-挂号列表等,perms: doctor.appointment/edit)
|
||||
*/
|
||||
public function edit()
|
||||
{
|
||||
$params = (new AppointmentValidate())->post()->goCheck('adminEdit');
|
||||
$post = $this->request->post();
|
||||
if (array_key_exists('assistant_id', $post)) {
|
||||
$params['assistant_id'] = $post['assistant_id'];
|
||||
}
|
||||
$result = AppointmentLogic::adminEdit($params, $this->adminId, $this->adminInfo);
|
||||
if ($result === false) {
|
||||
return $this->fail(AppointmentLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
/** 批量修改挂号渠道来源(消费者处方-挂号列表) */
|
||||
public function batchEditChannel()
|
||||
{
|
||||
$params = (new AppointmentValidate())->post()->goCheck('batchEditChannel');
|
||||
$post = $this->request->post();
|
||||
if (\array_key_exists('channel_source_detail', $post)) {
|
||||
$params['channel_source_detail'] = $post['channel_source_detail'];
|
||||
}
|
||||
$result = AppointmentLogic::adminBatchEditChannel($params, $this->adminId, $this->adminInfo);
|
||||
if ($result === false) {
|
||||
return $this->fail(AppointmentLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success((string) ($result['msg'] ?? '操作成功'), $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 预约详情
|
||||
* @return \think\response\Json
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+18
@@ -557,6 +557,24 @@ class OrderController extends BaseAdminController
|
||||
}
|
||||
}
|
||||
|
||||
public function setExempt()
|
||||
{
|
||||
$id = (int) $this->request->post('id', 0);
|
||||
$isExempt = (int) $this->request->post('is_exempt', 0);
|
||||
if ($id <= 0) {
|
||||
return $this->fail('参数错误');
|
||||
}
|
||||
if (!in_array($isExempt, [0, 1], true)) {
|
||||
return $this->fail('is_exempt 值无效');
|
||||
}
|
||||
$result = OrderLogic::setExempt($id, $isExempt);
|
||||
if (!$result) {
|
||||
return $this->fail(OrderLogic::getError());
|
||||
}
|
||||
$this->logOrderAction($id, 'set_exempt', $isExempt === 1 ? '设置豁免权' : '取消豁免权');
|
||||
return $this->success($isExempt === 1 ? '已设置豁免权' : '已取消豁免权');
|
||||
}
|
||||
|
||||
private function logOrderAction(int $orderId, string $action, string $summary = ''): void
|
||||
{
|
||||
OrderActionLogLogic::record($orderId, (int) $this->adminId, $this->adminInfo, $action, $summary);
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
|
||||
namespace app\adminapi\controller\stats;
|
||||
|
||||
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* 提成结算业绩(独立于业绩看板 yejiStats 接口)
|
||||
|
||||
*
|
||||
|
||||
* - GET stats.commissionSettlement/overview
|
||||
|
||||
* - GET stats.commissionSettlement/orderLines
|
||||
|
||||
* - GET stats.commissionSettlement/confirmStatus
|
||||
|
||||
* - POST stats.commissionSettlement/saveReconcile
|
||||
|
||||
* - POST stats.commissionSettlement/confirmFinalize
|
||||
|
||||
* - POST stats.commissionSettlement/confirmRevoke
|
||||
|
||||
* - GET stats.commissionSettlement/deptOptions
|
||||
|
||||
* - GET stats.commissionSettlement/channelOptions
|
||||
|
||||
*/
|
||||
|
||||
class CommissionSettlementController extends BaseAdminController
|
||||
|
||||
{
|
||||
|
||||
public function overview()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
$params = $this->request->get();
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementOverview($params, $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function orderLines()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementOrderLines($this->request->get(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmStatus()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementConfirmStatus($this->request->get(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function saveReconcile()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementSaveReconcile($this->request->post(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmFinalize()
|
||||
|
||||
{
|
||||
|
||||
@set_time_limit(120);
|
||||
|
||||
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementConfirmFinalize($this->request->post(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmRevoke()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::commissionSettlementConfirmRevoke($this->request->post(), $this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function deptOptions()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::deptOptions($this->adminId, $this->adminInfo));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function channelOptions()
|
||||
|
||||
{
|
||||
|
||||
return $this->data(YejiStatsLogic::channelOptions(false));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user