This commit is contained in:
Your Name
2026-05-12 17:58:54 +08:00
parent 796ca12fb8
commit 5d94ffab3e
3 changed files with 250 additions and 33 deletions
@@ -621,6 +621,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 +1161,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 +1250,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" />
@@ -1815,21 +1872,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">
@@ -3534,6 +3626,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 '顺丰速运'
@@ -4441,6 +4602,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 +4795,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)
}
}
//
+30 -14
View File
@@ -1319,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'
@@ -2080,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
@@ -2477,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
@@ -2532,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) {
@@ -2737,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
@@ -2978,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 {
@@ -3018,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 {
@@ -3060,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 = ''
@@ -3133,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 = ''
File diff suppressed because one or more lines are too long