Compare commits

...
47 Commits
Author SHA1 Message Date
Your Name 0fb51e12d1 更新 2026-05-19 10:18:00 +08:00
Your Name 4f9494db0b Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-18 18:12:23 +08:00
Your Name 4e87b080a0 更新 2026-05-18 18:12:04 +08:00
long 2f3038aba1 Merge branch '20260518-long' 2026-05-18 18:08:46 +08:00
long 131fb7d9c7 Update edit.vue
增加:账户消耗列表-编辑可以修改自媒体
2026-05-18 18:08:35 +08:00
Your Name 762a724204 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-18 18:02:15 +08:00
long 8090e0aab3 Merge branch '20260518-long' 2026-05-18 17:43:25 +08:00
long 9df6330d41 update
修改未服务天数判断条件
2026-05-18 17:42:24 +08:00
Your Name fd63e23eaf 更新 2026-05-18 16:56:00 +08:00
gr c1143f4629 geng
Merge branch 'master' of https://gitee.com/v_cms/zyt
2026-05-18 16:07:03 +08:00
Your Name 149fdffd49 更新 2026-05-18 15:59:22 +08:00
Your Name 439d5a3ec9 更新 2026-05-18 15:57:09 +08:00
long 9e24d49210 Merge branch '20260518-long' 2026-05-18 14:24:52 +08:00
long 90ac43fc82 update
更新自媒体渠道标签
2026-05-18 14:24:41 +08:00
gr 8b6b709987 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-18 11:36:21 +08:00
Your Name fb6ca9ce85 更新 2026-05-18 11:23:01 +08:00
Your Name 08290b329e 更新 2026-05-18 11:20:24 +08:00
long 1d66f84bd3 Update PatientOrderList.vue
业务订单增加:服务套餐
2026-05-18 10:16:31 +08:00
Your Name a2d9c1a03f Merge branch 'chufan-2026-5-16' 2026-05-18 09:43:48 +08:00
Your Name d79db88349 更新 2026-05-18 09:42:11 +08:00
Your Name 9f5fb650af 新增功能 2026-05-16 16:08:54 +08:00
Your Name 15b4339c90 更新 2026-05-16 16:05:29 +08:00
gr 5ce331394d Merge branch 'master' of https://gitee.com/v_cms/zyt
你好
2026-05-14 16:27:49 +08:00
Your Name 7ba9dabdb4 更新 2026-05-14 16:26:27 +08:00
Your Name 611f3dcd5c Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-14 16:23:52 +08:00
Your Name d3388b160e 更新 2026-05-14 16:23:29 +08:00
long f83d4c06cb Merge branch 'long-20260514' 2026-05-14 16:22:38 +08:00
long 4941ea3e21 Update DailyMatrix.vue
更新血糖折线图显示
2026-05-14 15:54:30 +08:00
Your Name e35696e153 更新 2026-05-14 15:34:28 +08:00
gr 800c6af7f1 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-14 11:31:40 +08:00
Your Name e1df584ed5 更新 2026-05-14 11:25:48 +08:00
gr 876ef0f8a0 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-14 11:02:29 +08:00
Your Name 928f75e016 更新 2026-05-14 11:00:56 +08:00
Your Name 37fa160c24 更新 2026-05-13 17:23:29 +08:00
Your Name 0eaacbb4ce 更新 2026-05-13 16:54:54 +08:00
Your Name 1e8b5c4646 修复bug 2026-05-13 09:49:47 +08:00
gr fdcfa30810 Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-12 18:15:10 +08:00
Your Name fabaa84373 新增 2026-05-12 18:14:20 +08:00
Your Name da017bdf20 更新 2026-05-12 18:10:40 +08:00
Your Name 5d94ffab3e 更新 2026-05-12 17:58:54 +08:00
Your Name 796ca12fb8 更新 2026-05-12 17:26:11 +08:00
gr 4caa98af3a Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-12 11:27:37 +08:00
Your Name c644f2554f 更新 2026-05-12 11:26:51 +08:00
gr bb87e4fb4c Merge branch 'master' of https://gitee.com/v_cms/zyt 2026-05-12 10:59:56 +08:00
Your Name e83dac756d 新增 2026-05-12 10:59:03 +08:00
gr 039105580c Mergt branch 'master' of https://gitee.com/v_cms/zyt 2026-05-11 17:49:51 +08:00
gr f47f431bdf 更新 2026-05-11 17:49:38 +08:00
5911 changed files with 5547 additions and 918 deletions
+10
View File
@@ -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 })
+20 -1
View File
@@ -110,6 +110,13 @@ 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
}) {
@@ -127,14 +134,21 @@ export function commissionSettlementChannelOptions() {
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
}
/** 提成核对:订单明细分页 bucket: 空|current|deferred */
/** 提成核对:订单明细分页 bucket: 空|current|deferredappt_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 },
@@ -160,3 +174,8 @@ export function commissionSettlementConfirmFinalize(params: Record<string, any>)
{ ignoreCancelToken: true }
)
}
/** 撤回「确定本期业绩」:清除顺延结转,状态变为可再次核对/确定 */
export function commissionSettlementConfirmRevoke(params: Record<string, any>) {
return request.post({ url: '/stats.commissionSettlement/confirmRevoke', params })
}
+20
View File
@@ -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>
+122 -2
View File
@@ -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)" />
@@ -1126,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要求完整手机号更易通过默认预填订单收货手机若仍提示验证码错误请改成与顺丰面单一致的号码
@@ -1200,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" />
@@ -1282,7 +1404,7 @@
<!-- 编辑分步向导与创建业务订单弹窗统一 -->
<el-dialog
v-model="editVisible"
title="编辑业务订单"
:title="editGancaoLogisticsOnlyMode ? '修改物流信息(甘草订单)' : '编辑业务订单'"
width="940px"
top="4vh"
:close-on-click-modal="false"
@@ -1305,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"
@@ -1325,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">
@@ -1471,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">
@@ -1648,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) }}
@@ -1661,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"
@@ -1779,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">
@@ -1815,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">
@@ -2165,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>
@@ -2375,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,
@@ -2383,12 +2588,14 @@ import {
prescriptionOrderDetail,
prescriptionOrderEdit,
prescriptionOrderLists,
prescriptionOrderExport,
prescriptionOrderPaidPayOrders,
prescriptionOrderWithdraw,
prescriptionOrderLogisticsTrace,
prescriptionOrderShip,
prescriptionOrderLogs,
prescriptionOrderUpdateAmount,
prescriptionOrderSetShipMode,
prescriptionOrderAddPayOrder,
prescriptionOrderComplete,
prescriptionOrderRevokeRxAudit,
@@ -2667,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
})
@@ -2731,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
@@ -2769,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)) {
@@ -2782,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
@@ -2980,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()
}
@@ -3083,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 }) {
@@ -3146,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)
@@ -3159,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) {
@@ -3534,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 '顺丰速运'
@@ -3728,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 = [
@@ -3906,6 +4285,8 @@ const editRules = computed<FormRules>(() => {
function resetEditOrderDialog() {
editOrderStep.value = 0
editOrderPrescription.value = null
editGancaoLogisticsOnlyMode.value = false
editGancaoDisplayNo.value = ''
editFormRef.value?.clearValidate()
}
@@ -4025,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
@@ -4041,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> = {
@@ -4244,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 || '')
@@ -4359,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}」已保存,是否立即确认发货?`,
@@ -4387,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('请先填写快递单号再确认发货')
@@ -4404,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()
})
@@ -4441,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
@@ -4603,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)
}
}
//
@@ -4795,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
})
@@ -3052,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 }) {
@@ -3128,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: {
@@ -4312,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}」已保存,是否立即确认发货?`,
+347 -37
View File
@@ -9,7 +9,11 @@
</span>
<h1 class="cs-masthead__title">提成结算业绩</h1>
<p class="cs-masthead__desc">
系统代开处方对应的已完成业务订单按结算月 7 日截止拆分本期提成与顺延下期可按部门汇总或下发至业绩归属医助与处方开方医生
订单池默认与<strong>处方订单列表</strong>同源<strong>创建时间 start_timeend_time</strong> +
<strong>fulfillment_status</strong>默认完成态 3账号<strong>列表可见性数据域</strong>一并生效
选结算月<strong>仍可</strong>套用物流签收<strong></strong>尾款支付<strong>均须在</strong><strong>结算月 7 24 </strong>拆分本期提成 / 顺延下期
仅有签收在范围内尾款晚于截止仍会顺延
展示部门渠道下拉与列表不同勾选后会与列表<strong>计数不同</strong>
</p>
</div>
<dl v-if="meta.settlement_month || meta.cutoff_end" class="cs-masthead__stats">
@@ -17,9 +21,9 @@
<dt class="cs-stat__label">结算月</dt>
<dd class="cs-stat__value">{{ meta.settlement_month || settlementMonth }}</dd>
</div>
<div v-if="meta.order_month" class="cs-stat">
<dt class="cs-stat__label">订单创建月</dt>
<dd class="cs-stat__value">{{ meta.order_month }}</dd>
<div v-if="meta.order_month || meta.order_start_time" class="cs-stat">
<dt class="cs-stat__label">订单时间窗</dt>
<dd class="cs-stat__value cs-stat__value--xs">{{ orderWindowLabel }}</dd>
</div>
<div v-if="meta.cutoff_end" class="cs-stat">
<dt class="cs-stat__label">截止时刻</dt>
@@ -42,6 +46,21 @@
class="filter-month"
/>
</div>
<div class="filter-field filter-field--rangewrap">
<span class="lbl">订单创建</span>
<el-date-picker
v-model="orderTimeRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator=""
start-placeholder="列表 start_time"
end-placeholder="列表 end_time"
class="filter-datetimerange"
/>
<el-checkbox v-model="requireSystemAutoOnly" class="filter-sa-check">
仅系统代开处方
</el-checkbox>
</div>
<div class="filter-field">
<span class="lbl">渠道来源</span>
<el-select
@@ -117,17 +136,26 @@
<div class="cs-alert__body">
<p v-if="meta.rule_note">{{ meta.rule_note }}</p>
<p v-else>
订单创建月为<strong>结算月的上一自然月</strong><strong>系统代开处方</strong>完成挂号等自动生成对应的<strong>履约已完成</strong>业务订单
<strong>签收</strong>时间取物流签收时间<strong>尾款支付</strong>取关联支付单已支付时间优先尾款/全部费用类型
签收与尾款均在<strong>结算月 7 24:00 </strong>完成的计入本期提成其余计入顺延下期
请选择结算月并设置<strong>订单创建时间范围</strong>与处方订单列表 URL 参数
<code>start_time</code>/<code>end_time</code>/<code>fulfillment_status</code>
一致切换结算月会自动带出<strong>上一自然月整段</strong>作为默认时段可按业务修改例如 2026-04-0204-30
仅系统代开处方勾选后与列表在未限制开方类型时的条数会不一致
</p>
<p v-if="meta.cutoff_end" class="cs-alert__meta">
当前结算月<strong>{{ meta.settlement_month }}</strong>
· 订单创建月<strong>{{ meta.order_month }}</strong>
· 订单时间<strong>{{ orderWindowLabel }}</strong>
· 履约状态<strong>{{ meta.fulfillment_status ?? 3 }}</strong>
· 截止时刻<strong>{{ meta.cutoff_end }}</strong>
<template v-if="meta.channel_name">
· 渠道<strong>{{ meta.channel_name }}</strong>
</template>
<template v-if="meta.settlement_month">
· 处方池<strong>{{
meta.require_system_auto_prescription ? '仅系统代开' : '含手动与系统'
}}</strong>
<template v-if="meta.explicit_order_time_window">
· 列表对齐<strong></strong></template>
</template>
</p>
</div>
</el-alert>
@@ -136,27 +164,43 @@
<div class="cs-reconcile">
<div class="cs-reconcile__head">
<div class="cs-reconcile__tags">
<el-tag v-if="confirmInfo?.status === 1" type="success" effect="dark">已确定业绩</el-tag>
<el-tag v-else type="warning" effect="plain">待确定业绩</el-tag>
<el-tag v-if="settlementConfirmedLocked" type="success" effect="dark">已确定业绩</el-tag>
<el-tag v-else-if="confirmInfo?.status === 2" type="warning" effect="plain">已撤回确定</el-tag>
<el-tag v-else type="info" effect="plain">待确定业绩</el-tag>
<span
v-if="confirmInfo?.status === 1 && confirmInfo.confirmed_at_text"
v-if="settlementConfirmedLocked && confirmInfo?.confirmed_at_text"
class="cs-reconcile__meta"
>
{{ confirmInfo.confirmed_admin_name || '—' }} · {{ confirmInfo.confirmed_at_text }}
</span>
<span
v-else-if="confirmInfo?.status === 2 && confirmInfo.revoked_at_text"
class="cs-reconcile__meta"
>
撤回{{ confirmInfo.revoked_admin_name || '—' }} · {{ confirmInfo.revoked_at_text }}
</span>
</div>
<div class="cs-reconcile__toolbar">
<el-button type="primary" plain @click="openReconcileDialog">业绩明细核对</el-button>
<el-button :disabled="confirmInfo?.status === 1 || reconcileSaving" @click="handleSaveReconcile">
<el-button :disabled="settlementConfirmedLocked || reconcileSaving" @click="handleSaveReconcile">
保存核对备注
</el-button>
<el-button
type="warning"
:disabled="confirmInfo?.status === 1 || confirmSubmitting"
:disabled="settlementConfirmedLocked || confirmSubmitting"
@click="handleConfirmFinalize"
>
确定本期业绩
</el-button>
<el-button
v-if="settlementConfirmedLocked"
type="danger"
plain
:loading="confirmRevoking"
@click="handleConfirmRevoke"
>
撤回确定
</el-button>
</div>
</div>
<el-input
@@ -166,7 +210,7 @@
maxlength="500"
show-word-limit
placeholder="核对备注(可与财务线下对齐后填写;确定业绩前可反复保存)"
:disabled="confirmInfo?.status === 1"
:disabled="settlementConfirmedLocked"
class="cs-reconcile__note"
/>
<p v-if="Number(totalAgg.carry_in_order_count) > 0" class="cs-reconcile__carry">
@@ -245,6 +289,8 @@
按诊单业绩归属医助 <strong>×</strong> 挂号渠道doctor_appointment.channels拆分到行
<strong>复诊2复诊3自媒体1等保持各自独立</strong>未关联挂号统一归到未匹配挂号渠道
同一医助多行<strong>自动合并医助 / 归属中心单元格</strong>顺延汇入为上期结转进入本期订单池的单数
<strong>展示部门</strong>有勾选时本表仅列出<strong>可归入该部门上下文</strong>的业绩医助与人事部门落在所选部门范围内的账号一致
<strong>点击挂号渠道</strong>查看该行对应的业务订单明细与上方部门筛选一致
</span>
</div>
<el-table
@@ -264,9 +310,10 @@
<template #default="{ row }">
<el-tag
size="small"
class="cs-channel-tag"
class="cs-channel-tag cs-channel-tag--clickable"
:class="channelTagClass(row.appt_channel_name)"
effect="plain"
@click.stop="openOrdersForAssistantChannel(row)"
>
{{ row.appt_channel_name }}
</el-tag>
@@ -293,6 +340,8 @@
<span class="cs-table-head__hint">
按处方创建账号开方人 <strong>×</strong> 挂号渠道拆分
可与医助维度交叉核对同一医生多行自动合并医生 / 归属中心单元格
<strong>展示部门</strong>有勾选时与医助表相同仅统计<strong>该部门上下文内</strong>订单对应的开方医生
<strong>点击挂号渠道</strong>查看该行对应的业务订单明细
</span>
</div>
<el-table
@@ -312,9 +361,10 @@
<template #default="{ row }">
<el-tag
size="small"
class="cs-channel-tag"
class="cs-channel-tag cs-channel-tag--clickable"
:class="channelTagClass(row.appt_channel_name)"
effect="plain"
@click.stop="openOrdersForDoctorChannel(row)"
>
{{ row.appt_channel_name }}
</el-tag>
@@ -341,7 +391,7 @@
<el-dialog
v-model="reconcileDialogVisible"
title="业绩明细核对"
width="1040px"
width="1280px"
destroy-on-close
class="cs-dialog"
@open="onReconcileDialogOpen"
@@ -354,7 +404,26 @@
</el-radio-group>
</div>
<el-table v-loading="linesLoading" :data="linesRows" border stripe size="small" max-height="440">
<el-table-column prop="order_no" label="业务订单号" min-width="116" show-overflow-tooltip />
<el-table-column prop="order_no" label="业务订单号" min-width="128" show-overflow-tooltip />
<el-table-column label="快递单号" min-width="132" show-overflow-tooltip>
<template #default="{ row }">{{ row.tracking_number || '—' }}</template>
</el-table-column>
<el-table-column label="挂号日期" min-width="124" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.appointment_date_text || '—' }}</span>
<el-tag
v-if="row.appointment_status && Number(row.appointment_status) !== 3"
class="cs-tag-appt-muted"
type="info"
size="small"
effect="plain"
>未完结挂号</el-tag
>
</template>
</el-table-column>
<el-table-column label="挂号渠道" min-width="124" show-overflow-tooltip>
<template #default="{ row }">{{ row.appt_channel_name || '—' }}</template>
</el-table-column>
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
<el-table-column prop="primary_dept_name" label="归属部门" min-width="100" show-overflow-tooltip />
@@ -373,8 +442,20 @@
<span v-else class="cs-muted"></span>
</template>
</el-table-column>
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
<el-table-column label="顺延说明" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.bucket === 'deferred' && row.bucket_defer_text" class="cs-muted">{{
row.bucket_defer_text
}}</span>
<span v-else class="cs-muted"></span>
</template>
</el-table-column>
<el-table-column label="物流签收" min-width="152" show-overflow-tooltip>
<template #default="{ row }">{{ row.sign_time_text || '—' }}</template>
</el-table-column>
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip>
<template #default="{ row }">{{ row.pay_time_text || '—' }}</template>
</el-table-column>
<el-table-column prop="create_time_text" label="订单创建" width="152" />
</el-table>
<div class="cs-dialog__pager">
@@ -390,8 +471,8 @@
<el-dialog
v-model="leafOrdersVisible"
:title="leafOrdersDept ? `已完成业绩订单 · ${leafOrdersDept.name}` : '已完成业绩订单'"
width="1040px"
:title="leafOrdersDialogTitle"
width="1280px"
destroy-on-close
class="cs-dialog"
>
@@ -403,12 +484,33 @@
</el-radio-group>
<span class="cs-dialog__hint">
结算月<strong>{{ meta.settlement_month || settlementMonth }}</strong>
<template v-if="meta.order_month"> · 订单创建月<strong>{{ meta.order_month }}</strong></template>
<template v-if="orderWindowLabel && orderWindowLabel !== '—'"
> · 订单创建<strong>{{ orderWindowLabel }}</strong></template
>
<template v-if="meta.channel_name"> · 渠道<strong>{{ meta.channel_name }}</strong></template>
</span>
</div>
<el-table v-loading="leafOrdersLoading" :data="leafOrdersRows" border stripe size="small" max-height="440">
<el-table-column prop="order_no" label="业务订单号" min-width="116" show-overflow-tooltip />
<el-table-column prop="order_no" label="业务订单号" min-width="128" show-overflow-tooltip />
<el-table-column label="快递单号" min-width="132" show-overflow-tooltip>
<template #default="{ row }">{{ row.tracking_number || '—' }}</template>
</el-table-column>
<el-table-column label="挂号日期" min-width="124" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.appointment_date_text || '—' }}</span>
<el-tag
v-if="row.appointment_status && Number(row.appointment_status) !== 3"
class="cs-tag-appt-muted"
type="info"
size="small"
effect="plain"
>未完结挂号</el-tag
>
</template>
</el-table-column>
<el-table-column label="挂号渠道" min-width="124" show-overflow-tooltip>
<template #default="{ row }">{{ row.appt_channel_name || '—' }}</template>
</el-table-column>
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
<el-table-column prop="primary_dept_name" label="归属部门" min-width="120" show-overflow-tooltip />
@@ -427,8 +529,20 @@
<span v-else class="cs-muted"></span>
</template>
</el-table-column>
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
<el-table-column label="顺延说明" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.bucket === 'deferred' && row.bucket_defer_text" class="cs-muted">{{
row.bucket_defer_text
}}</span>
<span v-else class="cs-muted"></span>
</template>
</el-table-column>
<el-table-column label="物流签收" min-width="152" show-overflow-tooltip>
<template #default="{ row }">{{ row.sign_time_text || '—' }}</template>
</el-table-column>
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip>
<template #default="{ row }">{{ row.pay_time_text || '—' }}</template>
</el-table-column>
<el-table-column prop="create_time_text" label="订单创建" width="152" />
</el-table>
<div class="cs-dialog__pager">
@@ -445,12 +559,13 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ArrowRight, ArrowUp, RefreshRight, Search, Tickets } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
commissionSettlementChannelOptions,
commissionSettlementConfirmFinalize,
commissionSettlementConfirmRevoke,
commissionSettlementDeptOptions,
commissionSettlementOrderLines,
commissionSettlementOverview,
@@ -565,6 +680,16 @@ interface CsDoctorRow {
deferred_period_amount: number
}
type LeafOrderDrillDept = { mode: 'dept'; dept_id: number; dept_name: string }
type LeafOrderDrillChannel = {
mode: 'channel'
title: string
assistant_id?: number
doctor_id?: number
appt_channel_value: number
}
type LeafOrderDrill = LeafOrderDrillDept | LeafOrderDrillChannel
interface CsAssistChannelRow extends CsAssistRow {
appt_channel_value: number
appt_channel_name: string
@@ -584,7 +709,27 @@ function defaultSettlementMonth(): string {
return `${y}-${m}`
}
function pad2(n: number): string {
return String(n).padStart(2, '0')
}
function formatDateTime(d: Date): string {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
}
/** 订单池默认:结算月「上一自然月」整段(与未传 start/end 的后端行为一致) */
function defaultOrderTimeRangeForSettlementMonth(sm: string): [string, string] {
const m = /^(\d{4})-(\d{2})$/.exec(sm)
const y = m ? parseInt(m[1], 10) : new Date().getFullYear()
const monthNum = m ? parseInt(m[2], 10) : new Date().getMonth() + 1
const start = new Date(y, monthNum - 2, 1, 0, 0, 0)
const end = new Date(y, monthNum - 1, 0, 23, 59, 59)
return [formatDateTime(start), formatDateTime(end)]
}
const settlementMonth = ref<string>(defaultSettlementMonth())
const orderTimeRange = ref<[string, string] | null>(defaultOrderTimeRangeForSettlementMonth(settlementMonth.value))
const requireSystemAutoOnly = ref(false)
const loading = ref(false)
const deptOptions = ref<DeptOption[]>([])
const deptOptionsLoading = ref(false)
@@ -612,9 +757,14 @@ const totalAgg = ref<{
const meta = reactive({
settlement_month: '',
order_month: '',
order_start_time: '',
order_end_time: '',
fulfillment_status: 3,
explicit_order_time_window: false,
cutoff_end: '',
rule_note: '',
channel_name: '',
require_system_auto_prescription: false,
})
const confirmInfo = ref<{
@@ -622,10 +772,17 @@ const confirmInfo = ref<{
reconcile_note?: string
confirmed_admin_name?: string
confirmed_at_text?: string
revoked_admin_name?: string
revoked_at_text?: string
is_confirmed_locked?: boolean
} | null>(null)
const reconcileNote = ref('')
const reconcileSaving = ref(false)
const confirmSubmitting = ref(false)
const confirmRevoking = ref(false)
/** 仅 status=1 时锁定,禁止保存备注 / 重复确定;status=2 已撤回可再操作 */
const settlementConfirmedLocked = computed(() => Number(confirmInfo.value?.status) === 1)
const reconcileDialogVisible = ref(false)
const linesLoading = ref(false)
@@ -636,7 +793,7 @@ const linesPage = ref(1)
const linesPageSize = ref(20)
const leafOrdersVisible = ref(false)
const leafOrdersDept = ref<{ id: number; name: string } | null>(null)
const leafOrderDrill = ref<LeafOrderDrill | null>(null)
const leafOrdersBucket = ref<string>('all')
const leafOrdersRows = ref<Record<string, any>[]>([])
const leafOrdersTotal = ref(0)
@@ -644,6 +801,20 @@ const leafOrdersPage = ref(1)
const leafOrdersPageSize = ref(20)
const leafOrdersLoading = ref(false)
const leafOrdersDialogTitle = computed(() => {
const d = leafOrderDrill.value
if (!d) return '已完成业绩订单'
if (d.mode === 'dept') return `已完成业绩订单 · ${d.dept_name}`
return d.title
})
const orderWindowLabel = computed(() => {
if (meta.order_start_time && meta.order_end_time) {
return `${meta.order_start_time}${meta.order_end_time}`
}
return meta.order_month || '—'
})
const deptChildrenMap = computed(() => {
const map = new Map<number, number[]>()
for (const d of deptOptions.value) {
@@ -718,7 +889,45 @@ function openOrderLinesForDept(deptId: number, deptName: string) {
ElMessage.warning('请选择结算月')
return
}
leafOrdersDept.value = { id: deptId, name: deptName }
leafOrderDrill.value = { mode: 'dept', dept_id: deptId, dept_name: deptName }
leafOrdersBucket.value = 'all'
leafOrdersPage.value = 1
leafOrdersRows.value = []
leafOrdersTotal.value = 0
leafOrdersVisible.value = true
loadLeafOrders(1)
}
function openOrdersForAssistantChannel(row: CsAssistChannelRow) {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
leafOrderDrill.value = {
mode: 'channel',
title: `业务订单 · ${row.appt_channel_name} · 医助 ${row.assistant_name}`,
assistant_id: row.assistant_id,
appt_channel_value: Number(row.appt_channel_value ?? 0),
}
leafOrdersBucket.value = 'all'
leafOrdersPage.value = 1
leafOrdersRows.value = []
leafOrdersTotal.value = 0
leafOrdersVisible.value = true
loadLeafOrders(1)
}
function openOrdersForDoctorChannel(row: CsDoctorChannelRow) {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
leafOrderDrill.value = {
mode: 'channel',
title: `业务订单 · ${row.appt_channel_name} · 医生 ${row.doctor_name}`,
doctor_id: row.doctor_id,
appt_channel_value: Number(row.appt_channel_value ?? 0),
}
leafOrdersBucket.value = 'all'
leafOrdersPage.value = 1
leafOrdersRows.value = []
@@ -728,18 +937,30 @@ function openOrderLinesForDept(deptId: number, deptName: string) {
}
async function loadLeafOrders(page: number) {
if (!settlementMonth.value || !leafOrdersDept.value) return
if (!settlementMonth.value || !leafOrderDrill.value) return
leafOrdersLoading.value = true
leafOrdersPage.value = page
try {
const d = leafOrderDrill.value
const p: Record<string, any> = {
settlement_month: settlementMonth.value,
dept_ids: String(leafOrdersDept.value.id),
page,
page_size: leafOrdersPageSize.value,
}
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
if (d.mode === 'dept') {
p.dept_ids = String(d.dept_id)
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
}
} else {
Object.assign(p, buildQueryParams())
p.appt_channel_value = d.appt_channel_value
if (d.assistant_id != null) {
p.assistant_id = d.assistant_id
}
if (d.doctor_id != null) {
p.doctor_id = d.doctor_id
}
}
if (leafOrdersBucket.value && leafOrdersBucket.value !== 'all') {
p.bucket = leafOrdersBucket.value
@@ -796,6 +1017,11 @@ async function loadOverview() {
ElMessage.warning('请选择结算月')
return
}
const ot = orderTimeRange.value
if (!ot || !ot[0] || !ot[1]) {
ElMessage.warning('请设置订单创建时间范围')
return
}
loading.value = true
try {
const res: any = await commissionSettlementOverview(buildQueryParams() as any)
@@ -807,9 +1033,21 @@ async function loadOverview() {
totalAgg.value = res?.total || {}
meta.settlement_month = res?.settlement_month || sm
meta.order_month = res?.order_month || ''
meta.order_start_time = res?.order_start_time ? String(res.order_start_time) : ''
meta.order_end_time = res?.order_end_time ? String(res.order_end_time) : ''
meta.fulfillment_status =
res?.fulfillment_status != null ? Number(res.fulfillment_status) : 3
meta.explicit_order_time_window =
typeof res?.explicit_order_time_window === 'boolean'
? res.explicit_order_time_window
: !!(meta.order_start_time && meta.order_end_time)
meta.cutoff_end = res?.cutoff_end || ''
meta.rule_note = res?.rule_note || ''
meta.channel_name = res?.channel_name || ''
meta.require_system_auto_prescription =
typeof res?.require_system_auto_prescription === 'boolean'
? res.require_system_auto_prescription
: false
confirmInfo.value = res?.confirm ?? null
reconcileNote.value =
res?.confirm?.reconcile_note != null ? String(res.confirm.reconcile_note) : ''
@@ -829,7 +1067,18 @@ async function loadOverview() {
function buildQueryParams(): Record<string, any> {
const sm = settlementMonth.value
const p: Record<string, any> = { settlement_month: sm }
const p: Record<string, any> = {
settlement_month: sm,
fulfillment_status: 3,
}
const ot = orderTimeRange.value
if (ot && ot[0] && ot[1]) {
p.start_time = ot[0]
p.end_time = ot[1]
}
if (requireSystemAutoOnly.value) {
p.require_system_auto_prescription = 1
}
if (selectedDeptIds.value.length > 0) {
p.dept_ids = selectedDeptIds.value.join(',')
}
@@ -882,8 +1131,8 @@ async function handleSaveReconcile() {
ElMessage.warning('请选择结算月')
return
}
if (confirmInfo.value?.status === 1) {
ElMessage.warning('已确定业绩不可修改备注')
if (settlementConfirmedLocked.value) {
ElMessage.warning('已确定业绩不可修改备注;请先撤回确定')
return
}
reconcileSaving.value = true
@@ -908,7 +1157,7 @@ async function handleConfirmFinalize() {
}
try {
await ElMessageBox.confirm(
'确定后将为「顺延下期」订单写入结转记录下一结算月在相同渠道与部门筛选下自动并入统计。确定后不可在线撤销,请与财务对后再操作。',
'确定后将为「顺延下期」订单写入结转记录下一结算月在相同渠道与部门筛选下自动并入统计。确定后仍可「撤回确定」以清除结转并重新核对(与财务对后再操作。',
'确定本期业绩',
{
confirmButtonText: '确定',
@@ -936,8 +1185,40 @@ async function handleConfirmFinalize() {
}
}
async function handleConfirmRevoke() {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
try {
await ElMessageBox.confirm(
'将撤回本次「确定业绩」状态,并删除已写入的顺延结转记录(下期将不再汇入这些单)。确定后继续?',
'撤回确定业绩',
{
confirmButtonText: '撤回',
cancelButtonText: '取消',
type: 'warning',
}
)
} catch {
return
}
confirmRevoking.value = true
try {
await commissionSettlementConfirmRevoke(buildQueryParams() as any)
ElMessage.success('已撤回确定;可重新保存备注并再次确定本期业绩')
await loadOverview()
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '撤回失败')
} finally {
confirmRevoking.value = false
}
}
function handleReset() {
settlementMonth.value = defaultSettlementMonth()
orderTimeRange.value = defaultOrderTimeRangeForSettlementMonth(settlementMonth.value)
requireSystemAutoOnly.value = false
selectedDeptIds.value = []
selectedChannel.value = ''
loadOverview()
@@ -1053,6 +1334,12 @@ function getSummaries(param: { columns: Array<{ property?: string }>; data: CsRo
return sums
}
watch(settlementMonth, (v) => {
if (v) {
orderTimeRange.value = defaultOrderTimeRangeForSettlementMonth(v)
}
})
onMounted(async () => {
await Promise.all([loadDeptOptions(), loadChannelOptions()])
await loadOverview()
@@ -1249,7 +1536,24 @@ onMounted(async () => {
flex: 1 1 280px;
min-width: 240px;
}
.filter-field--rangewrap {
flex: 1 1 440px;
min-width: 300px;
}
.filter-month { width: 168px; }
.filter-datetimerange {
width: 100%;
max-width: 520px;
}
.filter-sa-check {
margin-top: 6px;
font-weight: 500;
color: var(--cs-text);
}
.cs-tag-appt-muted {
margin-left: 6px;
vertical-align: middle;
}
.filter-select--channel { width: 220px; }
.filter-dept-tree-select { width: 100%; min-width: 240px; }
.filter-actions {
@@ -1581,6 +1885,12 @@ onMounted(async () => {
border-color: #cbd5e1 !important;
color: #64748b !important;
}
&--clickable {
cursor: pointer;
&:hover {
filter: brightness(0.96);
}
}
&--neutral {
background: #f0fdfa !important;
border-color: #99f6e4 !important;
+39 -2
View File
@@ -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()
}
+44 -44
View File
@@ -370,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复诊三<b>归属</b>按订单创建人的人事部门<b>合计业绩</b>列同口径非二中心子树部门为 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>
@@ -395,13 +395,8 @@
<el-tooltip placement="top" effect="dark" :show-after="200">
<template #content>
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
<<<<<<< HEAD
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
按订单<b>创建时间</b>统计剔除履约已取消/拒收/退款 4·9·10金额与<b>接诊诊单</b>列一致仅按<b>订单创建人</b>的人事部门落在本展示行含下级计入多行命中时取最深的展示部门<b>医助排行榜诊金</b>同口径<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表对齐<br />
=======
按订单<b>创建时间</b>统计剔除履约已取消金额与<b>接诊诊单</b>列一致<b>创建人</b>人事部门优先无创建人时回退诊单<b>医助</b>落在本展示行含下级即计入同表多行命中时取最深的展示部门<br />
<b>不受渠道筛选影响</b>与侧栏按部门打开的业务订单列表可对齐<br />
>>>>>>> master
点击金额可查看对应业务订单明细侧栏默认合计业绩全量列表
</div>
</template>
@@ -414,7 +409,7 @@
<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 />
<b>create_time</b> 归日剔除履约状态 4已取消/9拒收/10退款<br />
渠道判定用 EXISTS该患者有任一挂号 <b>channels 命中字典 + status=3</b>不限挂号时点<br />
点击金额可查看该部门本渠道订单明细侧栏渠道筛选列表
</div>
@@ -451,9 +446,9 @@
<template #content>
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
<<<<<<< HEAD
<b>业务订单条数</b>非取消订单 <b>create_time</b> 落入区间<b>fulfillment_status 4</b><b>订单创建人</b>的人事部门落在该部门子树即计入表格多行命中时取最深的展示部门<b>合计业绩 / 医助排行榜接诊诊单</b>同口径选定渠道时本列仍为全量
<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</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>
@@ -466,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>
@@ -979,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) }}
@@ -1241,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>
@@ -1323,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'
@@ -2084,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
@@ -2133,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(() => {
@@ -2481,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
@@ -2536,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) {
@@ -2645,7 +2647,7 @@ function buildOrderDrawerListParams(): Record<string, any> | null {
const params: Record<string, any> = {
start_time: `${f.start} 00:00:00`,
end_time: `${f.end} 23:59:59`,
/** 与业绩列口径一致:不列出履约已取消(4) */
/** 与业绩列口径一致:不列出履约 4(已取消)/9(拒收)/10(退款) */
exclude_fulfillment_cancelled: 1,
/**
* 业绩看板入口标志:后端 PrescriptionOrderLists::applyDoctorAssistantFilters 看到此标志后,
@@ -2656,14 +2658,6 @@ function buildOrderDrawerListParams(): Record<string, any> | null {
}
if (f.mode === 'dept') {
params.assistant_dept_id = f.deptId
<<<<<<< HEAD
=======
/** 与 YejiStatsLogic 最深展示行 + 创建人优先归属一致 */
if (f.yejiTableRowDeptIds && f.yejiTableRowDeptIds.length > 0) {
params.yeji_table_row_dept_ids = f.yejiTableRowDeptIds.join(',')
}
/** 勿传 yeji_drawer_match_table_performance:该参数按「业绩归因」医助收窄,与看板列表摊行不一致 */
>>>>>>> master
if (selectedDeptIds.value.length > 0) {
params.dept_ids = selectedDeptIds.value.join(',')
}
@@ -2749,8 +2743,10 @@ async function fetchOrderDrawerPage() {
) {
await hydrateOrderDrawerSplitPerfCache()
}
} 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 || '加载业务订单失败')
orderDrawerLists.value = []
orderDrawerCount.value = 0
orderDrawerExtend.value = null
@@ -2772,11 +2768,7 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: '
yejiTableRowDeptIds: tb.rows.map(r => r.dept_id).filter((id): id is number => typeof id === 'number' && id > 0),
}
orderDrawerTitle.value = `业务订单 · ${row.dept_name}`
<<<<<<< HEAD
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约已取消`
=======
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「接诊诊单」同口径:**创建人**人事部门优先,无创建人则诊单医助;同表多行间取最深归属 · 不含履约已取消`
>>>>>>> master
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 与「合计业绩 / 接诊诊单」同口径:订单创建人人事部门 ∈ 本部门子树 · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2800,7 +2792,7 @@ function openOrderDrawerByYejiTableScope(
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 个根节点(含其下全部展示行)· 不含履约已取消`
orderDrawerHint.value = `创建时间:${tb.start_date} ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2857,7 +2849,7 @@ function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['row
end,
}
orderDrawerTitle.value = `业务订单 · ${row.name}`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${row.name}(与排行榜诊金同口径)· 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2881,7 +2873,7 @@ function openOrderDrawerByAssistantErCenterRevisit(
}
const slotLbl = revisitSlot === 0 ? '复诊合计' : yejiRevisitSlotColumnTitle(revisitSlot)
orderDrawerTitle.value = `业务订单 · ${assistant.name} · ${slotLbl}`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约已取消`
orderDrawerHint.value = `创建时间:${start} ${end} · 创建人=${assistant.name}(与部门表 / 排行榜复诊同口径)· 仅二中心复诊口径 · ${slotLbl} · 不含履约 4/9/10`
orderDrawerPage.value = 1
orderDrawerVisible.value = true
void fetchOrderDrawerPage()
@@ -2994,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 {
@@ -3034,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 {
@@ -3076,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 = ''
@@ -3149,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 = ''
+39 -12
View File
@@ -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>
+63 -9
View File
@@ -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>
@@ -53,7 +53,7 @@
>
<el-option
v-for="item in channelOptions"
:key="item.value"
:key="String(item.value)"
:label="item.name"
:value="item.value"
/>
@@ -470,7 +470,11 @@ const loadChannelOptions = async () => {
if (ds !== 0) return ds
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
})
channelOptions.value = rows
// 统一为字符串,避免字典 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 = []
@@ -693,7 +697,7 @@ 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()
}
@@ -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 } {
@@ -274,6 +274,7 @@
<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>
@@ -332,7 +333,7 @@
</template>
<script setup lang="ts">
import { computed, watch, ref } from 'vue'
import { computed, watch, ref, onMounted } from 'vue'
import { usePaging } from '@/hooks/usePaging'
import {
prescriptionOrderLists,
@@ -341,6 +342,7 @@ import {
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'
@@ -563,6 +565,39 @@ function feeTypeText(t: number | undefined) {
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 '顺丰速运'
+28 -11
View File
@@ -257,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)">
@@ -797,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)
@@ -925,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,
+2 -2
View File
@@ -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>
File diff suppressed because one or more lines are too long
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+34
View File
@@ -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
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
@@ -32,6 +32,8 @@ use app\adminapi\logic\stats\YejiStatsLogic;
* - POST stats.commissionSettlement/confirmFinalize
* - POST stats.commissionSettlement/confirmRevoke
* - GET stats.commissionSettlement/deptOptions
* - GET stats.commissionSettlement/channelOptions
@@ -106,6 +108,16 @@ class CommissionSettlementController extends BaseAdminController
public function confirmRevoke()
{
return $this->data(YejiStatsLogic::commissionSettlementConfirmRevoke($this->request->post(), $this->adminId, $this->adminInfo));
}
public function deptOptions()
{
View File
View File
View File
View File
View File
View File
View File
View File
+21
View File
@@ -48,6 +48,27 @@ class PrescriptionController extends BaseAdminController
return $this->success('编辑成功');
}
/**
* @notes 修正处方患者姓名手机性别
*/
public function patchPatient()
{
$params = (new PrescriptionValidate())->post()->goCheck('patchPatient');
$ok = PrescriptionLogic::patchPatientContact(
(int) $params['id'],
(string) $params['patient_name'],
(string) $params['phone'],
(int) $params['gender'],
(int) $this->adminId,
$this->adminInfo
);
if (!$ok) {
return $this->fail(PrescriptionLogic::getError());
}
return $this->success('已更新');
}
/**
* @notes 删除处方
*/
View File
+27
View File
@@ -20,6 +20,14 @@ class PrescriptionOrderController extends BaseAdminController
return $this->dataLists(new PrescriptionOrderLists());
}
/**
* 处方业务订单导出(与 lists 相同筛选条件,Excel;参数 export=1 预估 export=2 下载)
*/
public function export()
{
return $this->dataLists(new PrescriptionOrderLists());
}
/**
* 指定诊单下可关联的支付单(待支付/已支付,创建/编辑业务订单时多选关联)
*/
@@ -116,6 +124,25 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('修改成功');
}
/**
* 设置发货类型(甘草 / 洛阳)
*/
public function setShipMode()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('setShipMode');
$result = PrescriptionOrderLogic::setShipMode(
(int) $params['id'],
(string) ($params['ship_mode'] ?? 'gancao'),
$this->adminId,
$this->adminInfo
);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('已保存', $result);
}
/**
* 修改关联处方的患者姓名与手机号(订单详情场景)
*/

Some files were not shown because too many files have changed in this diff Show More