This commit is contained in:
Your Name
2026-05-28 12:18:14 +08:00
parent afd88714ec
commit 7f08771dd0
9 changed files with 472 additions and 34 deletions
+1
View File
@@ -100,6 +100,7 @@ export function doctorDailyStatsOverview(params: {
start_date?: string
end_date?: string
channel_code?: string
dept_ids?: number[] | string
}) {
return request.get({ url: '/stats.doctorDailyStats/overview', params })
}
@@ -1707,6 +1707,8 @@ type AuxUsageForm = {
bags_per_dose: number
times_per_day: number
usage_days: number
/** 从处方库导入辅方时写入的模板名称,供药房联展示 */
prescription_name?: string
}
function defaultAuxUsage(prescriptionType = '浓缩水丸'): AuxUsageForm {
@@ -1753,7 +1755,8 @@ function normalizeAuxUsageForm(raw: unknown, prescriptionType: string): AuxUsage
need_decoction: o.need_decoction === 1 || o.need_decoction === true,
bags_per_dose: o.bags_per_dose != null ? Number(o.bags_per_dose) || 1 : base.bags_per_dose,
times_per_day: o.times_per_day != null ? Number(o.times_per_day) || 3 : base.times_per_day,
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days
usage_days: o.usage_days != null ? Number(o.usage_days) || 7 : base.usage_days,
prescription_name: String(o.prescription_name ?? '').trim()
}
}
@@ -3448,6 +3451,10 @@ const handleImportLibrary = (row: any) => {
} else {
editForm.herbs.push(...imported)
}
if (formulaType === '辅方') {
editForm.aux_usage.prescription_name = String(row.prescription_name ?? '').trim()
}
const modeHint = libraryImportMode.value === 'append' ? '(已追加)' : ''
if (findDuplicateHerbNamesLocal().length) {
feedback.msgWarning(
@@ -293,7 +293,7 @@
:fetch-fun="prescriptionOrderExport"
:params="prescriptionOrderExportParams"
:page-size="pager.size"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。"
export-hint="导出范围与上方筛选一致(履约状态、创建时间及其他条件均会生效)。含「自媒体渠道(挂号渠道来源)」:优先取该单关联处方登记的挂号;无则诊单下同患者挂号取 id 最大的一条(与前台挂号选择的记录一致);业绩侧栏带渠道筛选导出时与同页列表高亮挂号同源。「服务套餐」按字典 server_order 解析展示;「签收日期」与详情/业绩看板提成口径一致:以快递主表 sign_time 为主,无效时回退至签收类轨迹时间或已签收状态下的最新轨迹时间。"
/>
</el-form-item>
</el-form>
@@ -2449,7 +2449,9 @@
</div>
</template>
<template v-if="slipAuxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
<div class="rx-herb-section-label rx-herb-section-label--aux">
辅方<span v-if="slipAuxLibraryName" class="rx-herb-library-name">{{ slipAuxLibraryName }}</span>
</div>
<div
v-for="(h, i) in slipAuxHerbs"
:key="'aux-' + i"
@@ -2461,10 +2463,28 @@
</template>
</template>
<template v-else>
<div v-for="(h, i) in slipHerbsList" :key="i" class="rx-herb-cell">
<span class="rx-herb-name">{{ h.name }}</span>
<span class="rx-herb-total">{{ h.dosage }}</span>
</div>
<template v-if="slipMainHerbs.length">
<div class="rx-herb-section-label">主方</div>
<div
v-for="(h, i) in slipMainHerbs"
:key="'user-main-' + i"
class="rx-herb-cell"
>
<span class="rx-herb-name">{{ h.name }}</span>
<span class="rx-herb-total">{{ h.dosage }}</span>
</div>
</template>
<template v-if="slipAuxHerbs.length">
<div class="rx-herb-section-label rx-herb-section-label--aux">辅方</div>
<div
v-for="(h, i) in slipAuxHerbs"
:key="'user-aux-' + i"
class="rx-herb-cell"
>
<span class="rx-herb-name">{{ h.name }}</span>
<span class="rx-herb-total">{{ h.dosage }}</span>
</div>
</template>
</template>
</div>
</div>
@@ -2714,6 +2734,7 @@ import {
prescriptionOrderSubmitGancaoRecipel,
prescriptionOrderPreviewGancaoRecipel,
prescriptionDetail,
prescriptionLibraryLists,
getDoctors,
getAssistants
} from '@/api/tcm'
@@ -5480,6 +5501,8 @@ const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄
const prescriptionViewVisible = ref(false)
const prescriptionViewLoading = ref(false)
const prescriptionViewData = ref<any>(null)
/** 药房联辅方标题:处方库中该辅方模板的 prescription_name(导入或药材匹配解析) */
const slipAuxLibraryName = ref('')
const prescriptionTabType = ref('internal')
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
const prescriptionSlipExporting = ref(false)
@@ -5883,6 +5906,60 @@ function openPrescriptionViewFromEdit() {
openPrescriptionView(editForm)
}
function herbSetMatchKey(herbs: Array<{ name?: unknown; dosage?: unknown }>): string {
return herbs
.map((h) => `${String(h.name ?? '').trim()}:${Number(h.dosage) || 0}`)
.sort()
.join('|')
}
function herbsMatchPrescriptionLibrary(
prescriptionHerbs: Array<{ name?: unknown; dosage?: unknown }>,
libraryHerbs: Array<{ name?: unknown; dosage?: unknown }>
): boolean {
if (!prescriptionHerbs.length || !libraryHerbs.length) return false
if (prescriptionHerbs.length !== libraryHerbs.length) return false
return herbSetMatchKey(prescriptionHerbs) === herbSetMatchKey(libraryHerbs)
}
function readAuxLibraryNameFromUsage(raw: unknown): string {
if (!raw || typeof raw !== 'object') return ''
const o = raw as Record<string, unknown>
return String(o.prescription_name ?? o.library_name ?? '').trim()
}
/** 解析药房联辅方对应的处方库名称:优先 aux_usage 内持久化字段,否则按开方医师处方库辅方模板药材精确匹配 */
async function resolveSlipAuxLibraryName(data: Record<string, unknown>): Promise<string> {
const fromUsage = readAuxLibraryNameFromUsage(data.aux_usage)
if (fromUsage) return fromUsage
const herbs = Array.isArray(data.herbs) ? data.herbs : []
const auxHerbs = herbs.filter((h) => normalizeSlipFormulaType((h as any)?.formula_type) === '辅方')
if (!auxHerbs.length) return ''
const doctorId = Number(data.creator_id) > 0 ? Number(data.creator_id) : 0
if (!doctorId) return ''
try {
const res: any = await prescriptionLibraryLists({
page_no: 1,
page_size: 200,
formula_type: '辅方',
prescribing_creator_id: doctorId
})
const lists = Array.isArray(res?.lists) ? res.lists : []
for (const row of lists) {
const libHerbs = Array.isArray(row?.herbs) ? row.herbs : []
if (herbsMatchPrescriptionLibrary(auxHerbs, libHerbs)) {
return String(row?.prescription_name ?? '').trim()
}
}
} catch {
/* 静默:无匹配时仅展示「辅方」 */
}
return ''
}
async function openPrescriptionView(row: any) {
if (!row.prescription_id) {
feedback.msgWarning('该订单未关联处方')
@@ -5892,6 +5969,7 @@ async function openPrescriptionView(row: any) {
prescriptionViewVisible.value = true
prescriptionViewLoading.value = true
prescriptionViewData.value = null
slipAuxLibraryName.value = ''
prescriptionTabType.value = 'internal'
try {
@@ -5926,12 +6004,15 @@ async function openPrescriptionView(row: any) {
merged.appointment_id = Number(linkedAp.id)
}
prescriptionViewData.value = merged
slipAuxLibraryName.value = await resolveSlipAuxLibraryName(merged)
} else {
prescriptionViewData.value = null
slipAuxLibraryName.value = ''
}
} catch (e: any) {
feedback.msgError(e?.message || '加载处方详情失败')
prescriptionViewVisible.value = false
slipAuxLibraryName.value = ''
} finally {
prescriptionViewLoading.value = false
}
@@ -6585,6 +6666,11 @@ async function downloadPrescriptionSlipPdf() {
margin-top: 4px;
}
.rx-herb-library-name {
font-weight: 500;
margin-left: 2px;
}
.rx-herb-cell {
display: grid;
grid-template-columns: 1fr 64px;
+67
View File
@@ -181,9 +181,44 @@
<template #default="{ row }">¥ {{ formatDoctorDailyMoney(row.deal_amount) }}</template>
</el-table-column>
<el-table-column prop="deal_order_count" label="接诊诊单" min-width="88" align="right" />
<el-table-column
prop="appointment_total"
label="总挂号"
min-width="88"
align="right"
>
<template #header>
<span>总挂号</span>
<el-tooltip
effect="dark"
placement="top"
content="区间内该医生 doctor_appointment 全部状态合计(已预约/已完成/已过号/已取消);选部门时仅计经手医助归属在所选部门子树的挂号(与业绩看板「预约诊单」医助口径一致)"
>
<el-icon class="doctor-daily-th__hint"><InfoFilled /></el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="appointment_completed" label="挂号完成" min-width="88" align="right" />
<el-table-column prop="appointment_missed" label="过号" min-width="72" align="right" />
<el-table-column prop="appointment_cancelled" label="取消" min-width="72" align="right" />
<el-table-column
prop="appointment_conversion_rate"
label="挂号率"
min-width="92"
align="right"
>
<template #header>
<span>挂号率</span>
<el-tooltip
effect="dark"
placement="top"
content="挂号率 = 成交单数 / 总挂号数 × 100;总挂号为 0 时显示「—」"
>
<el-icon class="doctor-daily-th__hint"><InfoFilled /></el-icon>
</el-tooltip>
</template>
<template #default="{ row }">{{ formatDoctorDailyRate(row.appointment_conversion_rate) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-empty v-else description="当前账号无医生统计权限" />
@@ -2078,6 +2113,10 @@ function buildDoctorDailyRequestParams(): Record<string, any> {
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
}
// 选中父级时由后端 resolvePrimaryDepts 自动展开所有子级,再用 admin_dept 收窄医生集合。
if (selectedDeptIds.value.length > 0) {
p.dept_ids = selectedDeptIds.value.join(',')
}
if (customRange.value?.[0] && customRange.value?.[1]) {
p.start_date = customRange.value[0]
p.end_date = customRange.value[1]
@@ -2096,6 +2135,15 @@ function formatDoctorDailyMoney(n: number | string | undefined) {
return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
/** 挂号率展示:null 或非有限数显示「—」;保留 2 位小数 + % 后缀 */
function formatDoctorDailyRate(v: number | string | null | undefined): string {
if (v === null || v === undefined || v === '') return '—'
const n = Number(v)
if (!Number.isFinite(n)) return '—'
return `${n.toFixed(2)}%`
}
function getDoctorDailySummaries(param: { columns: any[] }) {
const t = doctorDailyTotal.value
const sums: string[] = []
@@ -2118,6 +2166,9 @@ function getDoctorDailySummaries(param: { columns: any[] }) {
case 'deal_order_count':
sums[index] = String(t.deal_order_count ?? 0)
break
case 'appointment_total':
sums[index] = String(t.appointment_total ?? 0)
break
case 'appointment_completed':
sums[index] = String(t.appointment_completed ?? 0)
break
@@ -2127,6 +2178,9 @@ function getDoctorDailySummaries(param: { columns: any[] }) {
case 'appointment_cancelled':
sums[index] = String(t.appointment_cancelled ?? 0)
break
case 'appointment_conversion_rate':
sums[index] = formatDoctorDailyRate(t.appointment_conversion_rate)
break
default:
sums[index] = ''
}
@@ -4100,6 +4154,19 @@ onMounted(async () => {
background: var(--yj-accent-soft) !important;
font-size: 13px;
}
.doctor-daily-th__hint {
margin-left: 4px;
color: var(--yj-muted);
cursor: help;
vertical-align: -2px;
font-size: 13px;
transition: color 0.15s;
&:hover {
color: var(--yj-brand);
}
}
}
.filter-surface {