更新
This commit is contained in:
+375
-218
@@ -317,75 +317,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||
<p class="yeji-zyyt-intro">
|
||||
各中心部门诊金、业绩、进线与 ROI 等,与顶部渠道、展示部门、日期筛选联动。本月 / 本周 / 今日等多张业绩表中「预约诊单」列数字可点开查看每条挂号记录。
|
||||
</p>
|
||||
|
||||
<!-- <el-alert
|
||||
v-if="channelFilterNote"
|
||||
:title="channelFilterNote"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="filter-alert"
|
||||
/> -->
|
||||
<!-- ── 目标进度卡片 ── -->
|
||||
<div
|
||||
v-if="targetProgressCard || targetProgressLoading"
|
||||
v-loading="targetProgressLoading"
|
||||
class="tp-wrap"
|
||||
>
|
||||
<div v-if="targetProgressCard" class="tp-card">
|
||||
<div class="tp-card__title">{{ targetProgressCard.title }}</div>
|
||||
<div class="tp-card__body">
|
||||
<div
|
||||
v-for="sec in targetProgressCard.sections"
|
||||
:key="sec.dept_id"
|
||||
class="tp-section"
|
||||
>
|
||||
<div class="tp-section__head">
|
||||
<div class="tp-section__title-col">
|
||||
<span v-if="sec.parent_dept_name" class="tp-section__parent">{{
|
||||
sec.parent_dept_name
|
||||
}}</span>
|
||||
<span class="tp-section__name">{{ sec.dept_name }}目标</span>
|
||||
</div>
|
||||
<span class="tp-section__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
||||
</div>
|
||||
<table class="tp-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tp-lbl">昨日诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.yesterday_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.douyin_amount !== null">
|
||||
<td class="tp-lbl">抖音总诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.douyin_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.tuifen_amount !== null">
|
||||
<td class="tp-lbl">推粉总诊金</td>
|
||||
<td class="tp-val">{{ formatInt(sec.tuifen_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">总诊金</td>
|
||||
<td class="tp-val tp-val--total">{{ formatInt(sec.total_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">目标进度</td>
|
||||
<td class="tp-val">{{ formatMoney(sec.target_progress) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-lbl">完成进度</td>
|
||||
<td class="tp-val" :class="achievementClass(sec.completion_rate)">{{ formatAchievement(sec.completion_rate) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 业绩表(4 张/N 张) ────────────────── -->
|
||||
<!-- ── 业绩表(4 张/N 张);目标看板见「目标看板(卡片)」Tab ── -->
|
||||
<div v-loading="loading" class="tables-wrap">
|
||||
<div v-if="!loading && tables.length === 0" class="empty-tip">
|
||||
<el-empty description="暂无数据" />
|
||||
@@ -464,7 +396,8 @@
|
||||
<template #content>
|
||||
<div style="max-width: 300px; line-height: 1.7; font-size: 12px">
|
||||
按订单<b>创建时间</b>统计(剔除履约已取消),金额与<b>接诊诊单</b>列一致:诊单<b>医助</b>或订单<b>创建人</b>的人事部门落在本展示行(含下级)即计入,多行命中时取最深的展示部门。<br />
|
||||
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表可对齐。
|
||||
<b>不受渠道筛选影响</b>。与侧栏按部门打开的业务订单列表可对齐。<br />
|
||||
点击金额可查看对应业务订单明细(侧栏默认「合计业绩」全量列表)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -477,7 +410,8 @@
|
||||
<div style="max-width: 340px; line-height: 1.7; font-size: 12px">
|
||||
「{{ tb.channel_name }}」渠道业绩(与卡片标题渠道一致)。与「处方订单列表」业绩<b>同口径</b>:按订单
|
||||
<b>create_time</b> 归日,剔除履约已取消(4)。<br />
|
||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。
|
||||
渠道判定用 EXISTS:该患者有任一挂号 <b>channels 命中字典 + status=3</b>(不限挂号时点)。<br />
|
||||
点击金额可查看该部门本渠道订单明细(侧栏「渠道筛选」列表)。
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info" aria-label="说明"><InfoFilled /></el-icon>
|
||||
@@ -611,8 +545,25 @@
|
||||
>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</span>
|
||||
<template v-else>{{ formatInt(yejiRevisitSlotAt(r, slot)) }}</template>
|
||||
</td>
|
||||
<td>{{ formatMoney(r.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(r.completed_performance_amount) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="yejiPerfCellDrawerEligible(tb, r, 'total')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceCellClick(tb, r, 'total')"
|
||||
>{{ formatMoney(r.performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(r.performance_amount) }}</template>
|
||||
</td>
|
||||
<td
|
||||
v-if="tb.channel_name"
|
||||
class="col-completed"
|
||||
>
|
||||
<span
|
||||
v-if="yejiPerfCellDrawerEligible(tb, r, 'channel')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceCellClick(tb, r, 'channel')"
|
||||
>{{ formatMoney(r.completed_performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(r.completed_performance_amount) }}</template>
|
||||
</td>
|
||||
<td @click.stop="onYejiDeptAppointmentBookedClick(tb, r)">
|
||||
<span
|
||||
v-if="
|
||||
@@ -639,8 +590,25 @@
|
||||
v-for="slot in yejiRevisitSlotColumns"
|
||||
:key="'yjvt-' + idx + '-' + slot"
|
||||
>{{ formatInt(yejiRevisitSlotAt(tb.total, slot)) }}</td>
|
||||
<td>{{ formatMoney(tb.total.performance_amount) }}</td>
|
||||
<td v-if="tb.channel_name" class="col-completed">{{ formatMoney(tb.total.completed_performance_amount) }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="yejiPerfTotalRowDrawerEligible(tb, 'total')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceTotalRowClick(tb, 'total')"
|
||||
>{{ formatMoney(tb.total.performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(tb.total.performance_amount) }}</template>
|
||||
</td>
|
||||
<td
|
||||
v-if="tb.channel_name"
|
||||
class="col-completed"
|
||||
>
|
||||
<span
|
||||
v-if="yejiPerfTotalRowDrawerEligible(tb, 'channel')"
|
||||
class="yeji-lead-cell--link"
|
||||
@click.stop="onYejiPerformanceTotalRowClick(tb, 'channel')"
|
||||
>{{ formatMoney(tb.total.completed_performance_amount) }}</span>
|
||||
<template v-else>{{ formatMoney(tb.total.completed_performance_amount) }}</template>
|
||||
</td>
|
||||
<td>{{ formatInt(tb.total.appointment_booked_count ?? 0) }}</td>
|
||||
<td>{{ formatInt(tb.total.consult_count) }}</td>
|
||||
<td>{{ formatInt(tb.total.deal_order_count) }}</td>
|
||||
@@ -663,6 +631,72 @@
|
||||
</div>
|
||||
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="目标看板(卡片)" name="zyyt_matrix" lazy>
|
||||
<div v-loading="targetProgressLoading" class="tp-matrix-wrap">
|
||||
<template v-if="targetProgressCard">
|
||||
<h2 class="tp-matrix__title">{{ targetProgressCard.title }}</h2>
|
||||
<div class="tp-matrix" role="list">
|
||||
<div
|
||||
v-for="sec in targetProgressCard.sections"
|
||||
:key="'tm-' + sec.dept_id"
|
||||
class="tp-matrix-card"
|
||||
role="listitem"
|
||||
>
|
||||
<div class="tp-matrix-card__head">
|
||||
<div class="tp-matrix-card__titles">
|
||||
<span v-if="sec.parent_dept_name" class="tp-matrix-card__parent">{{
|
||||
sec.parent_dept_name
|
||||
}}</span>
|
||||
<span class="tp-matrix-card__name">{{ sec.dept_name }}目标</span>
|
||||
</div>
|
||||
<span class="tp-matrix-card__target">{{ formatMoneyW(sec.target_amount) }}</span>
|
||||
</div>
|
||||
<table class="tp-matrix-card__table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">昨日诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.yesterday_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.douyin_amount !== null">
|
||||
<td class="tp-matrix-card__lbl">抖音总诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.douyin_amount) }}</td>
|
||||
</tr>
|
||||
<tr v-if="sec.tuifen_amount !== null">
|
||||
<td class="tp-matrix-card__lbl">推粉总诊金</td>
|
||||
<td class="tp-matrix-card__num">{{ formatInt(sec.tuifen_amount) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">总诊金</td>
|
||||
<td class="tp-matrix-card__num tp-matrix-card__num--hot">
|
||||
{{ formatInt(sec.total_amount) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">目标进度</td>
|
||||
<td class="tp-matrix-card__num">{{ formatMoney(sec.target_progress) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tp-matrix-card__lbl">完成进度</td>
|
||||
<td
|
||||
class="tp-matrix-card__num tp-matrix-card__num--hot"
|
||||
:class="achievementClass(sec.completion_rate)"
|
||||
>
|
||||
{{ formatAchievement(sec.completion_rate) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty
|
||||
v-else-if="!targetProgressLoading"
|
||||
class="tp-matrix-empty"
|
||||
description="暂无目标数据,请先配置当月部门目标或为部门设置诊金目标后刷新页面"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
@@ -1623,7 +1657,7 @@ const appointmentLinesShowChannelColumn = computed(() =>
|
||||
)
|
||||
|
||||
/** 图表 / 数据表切换 */
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt'>('zyyt')
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'>('zyyt')
|
||||
|
||||
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
|
||||
const userStore = useUserStore()
|
||||
@@ -2586,40 +2620,79 @@ function formatOrderDrawerTime(t: unknown): string {
|
||||
return s.length >= 16 ? s.slice(0, 16) : s
|
||||
}
|
||||
|
||||
async function fetchOrderDrawerPage() {
|
||||
/** 业务订单侧栏列表筛选(不含分页) */
|
||||
function buildOrderDrawerListParams(): Record<string, any> | null {
|
||||
const f = orderDrawerFilter.value
|
||||
if (!f) return
|
||||
if (!f) return null
|
||||
const params: Record<string, any> = {
|
||||
start_time: `${f.start} 00:00:00`,
|
||||
end_time: `${f.end} 23:59:59`,
|
||||
/** 与业绩列口径一致:不列出履约已取消(4) */
|
||||
exclude_fulfillment_cancelled: 1,
|
||||
/** 侧栏展示约诊数等 extend 字段 */
|
||||
yeji_order_drawer: 1,
|
||||
}
|
||||
if (f.mode === 'dept') {
|
||||
params.assistant_dept_id = f.deptId
|
||||
/** 勿传 yeji_drawer_match_table_performance:该参数按「业绩归因」医助收窄,与看板渠道成交/渠道业绩的列表摊行(诊单医助∪创建人∪最深展示行)不一致 */
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
params.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
} else {
|
||||
params.assistant_id = f.assistantId
|
||||
if (f.erCenterRevisitSlot !== undefined) {
|
||||
params.yeji_er_center_revisit_only = 1
|
||||
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerVisible.value &&
|
||||
orderDrawerListTab.value === 'channel'
|
||||
) {
|
||||
params.channel_code = selectedChannel.value
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
async function hydrateOrderDrawerSplitPerfCache() {
|
||||
const base = buildOrderDrawerListParams()
|
||||
if (!base || !selectedChannel.value) return
|
||||
const params: Record<string, any> = {
|
||||
...base,
|
||||
channel_code: selectedChannel.value,
|
||||
page_no: 1,
|
||||
page_size: 1,
|
||||
}
|
||||
try {
|
||||
const res: any = await prescriptionOrderLists(params)
|
||||
const ex = res?.extend && typeof res.extend === 'object' ? res.extend : null
|
||||
if (ex && ex.stats_yeji_drawer_total_performance_amount != null) {
|
||||
orderDrawerSplitPerfCache.value = {
|
||||
totalAmount: Number(ex.stats_yeji_drawer_total_performance_amount),
|
||||
totalOrderCount: Number(ex.stats_yeji_drawer_total_performance_order_count ?? 0),
|
||||
channelAmount: Number(ex.stats_order_amount_performance ?? 0),
|
||||
channelOrderCount: Number(res?.count ?? 0),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 对照指标失败不打断列表
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrderDrawerPage() {
|
||||
const base = buildOrderDrawerListParams()
|
||||
if (!base) return
|
||||
if (selectedChannel.value && orderDrawerListTab.value === 'total') {
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
}
|
||||
orderDrawerLoading.value = true
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
...base,
|
||||
page_no: orderDrawerPage.value,
|
||||
page_size: orderDrawerPageSize.value,
|
||||
start_time: `${f.start} 00:00:00`,
|
||||
end_time: `${f.end} 23:59:59`,
|
||||
/** 与业绩列口径一致:不列出履约已取消(4) */
|
||||
exclude_fulfillment_cancelled: 1,
|
||||
/** 侧栏展示约诊数等 extend 字段 */
|
||||
yeji_order_drawer: 1,
|
||||
}
|
||||
if (f.mode === 'dept') {
|
||||
params.assistant_dept_id = f.deptId
|
||||
/** 与看板「接诊诊单」列一致:处方列表默认 assistant_dept_id(诊单医助或创建人∪子树),不用业绩摊行医助集合 */
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
params.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
} else {
|
||||
params.assistant_id = f.assistantId
|
||||
if (f.erCenterRevisitSlot !== undefined) {
|
||||
params.yeji_er_center_revisit_only = 1
|
||||
params.yeji_er_center_revisit_slot = f.erCenterRevisitSlot
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerChannelSplitActive.value &&
|
||||
orderDrawerListTab.value === 'channel'
|
||||
) {
|
||||
params.channel_code = selectedChannel.value
|
||||
}
|
||||
const res: any = await prescriptionOrderLists(params)
|
||||
orderDrawerLists.value = Array.isArray(res?.lists) ? res.lists : []
|
||||
@@ -2640,6 +2713,13 @@ async function fetchOrderDrawerPage() {
|
||||
channelOrderCount: Number(res?.count ?? 0),
|
||||
}
|
||||
}
|
||||
if (
|
||||
selectedChannel.value &&
|
||||
orderDrawerVisible.value &&
|
||||
orderDrawerListTab.value === 'total'
|
||||
) {
|
||||
await hydrateOrderDrawerSplitPerfCache()
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.msg || e?.message || '加载业务订单失败')
|
||||
orderDrawerLists.value = []
|
||||
@@ -2650,8 +2730,9 @@ async function fetchOrderDrawerPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
function openOrderDrawerByDept(tb: YejiTable, row: YejiRow, opts?: { listTab?: 'total' | 'channel' }) {
|
||||
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||
orderDrawerListTab.value = channelSplit ? opts?.listTab ?? 'channel' : 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'dept',
|
||||
@@ -2667,6 +2748,69 @@ function openOrderDrawerByDept(tb: YejiTable, row: YejiRow) {
|
||||
void fetchOrderDrawerPage()
|
||||
}
|
||||
|
||||
/** 顶部「展示部门」仅选中 1 个根节点时:合计行业绩对应该子树下的订单汇总 */
|
||||
function openOrderDrawerByYejiTableScope(
|
||||
tb: YejiTable,
|
||||
rootDeptId: number,
|
||||
listTab: 'total' | 'channel'
|
||||
) {
|
||||
const channelSplit = Boolean(selectedChannel.value && tb.channel_name)
|
||||
orderDrawerListTab.value = channelSplit ? listTab : 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
orderDrawerFilter.value = {
|
||||
mode: 'dept',
|
||||
deptId: rootDeptId,
|
||||
deptName: '当前展示合计',
|
||||
start: tb.start_date,
|
||||
end: tb.end_date,
|
||||
}
|
||||
orderDrawerTitle.value = '业务订单 · 当前展示合计'
|
||||
orderDrawerHint.value = `创建时间:${tb.start_date} ~ ${tb.end_date} · 已选展示部门 1 个根节点(含其下全部展示行)· 不含履约已取消`
|
||||
orderDrawerPage.value = 1
|
||||
orderDrawerVisible.value = true
|
||||
void fetchOrderDrawerPage()
|
||||
}
|
||||
|
||||
function yejiPerfCellDrawerEligible(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel'): boolean {
|
||||
if (isUnassignedYejiRow(row) || row.dept_id <= 0) {
|
||||
return false
|
||||
}
|
||||
if (kind === 'channel' && !tb.channel_name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function yejiPerfTotalRowDrawerEligible(tb: YejiTable, kind: 'total' | 'channel'): boolean {
|
||||
if (selectedDeptIds.value.length !== 1) {
|
||||
return false
|
||||
}
|
||||
if (kind === 'channel' && !tb.channel_name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function onYejiPerformanceCellClick(tb: YejiTable, row: YejiRow, kind: 'total' | 'channel') {
|
||||
if (!yejiPerfCellDrawerEligible(tb, row, kind)) {
|
||||
return
|
||||
}
|
||||
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||
openOrderDrawerByDept(tb, row, { listTab })
|
||||
}
|
||||
|
||||
function onYejiPerformanceTotalRowClick(tb: YejiTable, kind: 'total' | 'channel') {
|
||||
if (!yejiPerfTotalRowDrawerEligible(tb, kind)) {
|
||||
return
|
||||
}
|
||||
const rootDeptId = selectedDeptIds.value[0]
|
||||
if (!rootDeptId) {
|
||||
return
|
||||
}
|
||||
const listTab: 'total' | 'channel' = kind === 'total' ? 'total' : 'channel'
|
||||
openOrderDrawerByYejiTableScope(tb, rootDeptId, listTab)
|
||||
}
|
||||
|
||||
function openOrderDrawerByAssistant(row: LeaderboardPack['leaderboards'][0]['rows'][0], start: string, end: string) {
|
||||
orderDrawerListTab.value = 'channel'
|
||||
orderDrawerSplitPerfCache.value = null
|
||||
@@ -3946,118 +4090,7 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 目标进度卡片 ── */
|
||||
.tp-wrap {
|
||||
min-height: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tp-card {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
border-radius: var(--yj-radius);
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
background: var(--yj-surface);
|
||||
border: 1px solid var(--yj-line);
|
||||
box-shadow: var(--yj-shadow-md);
|
||||
|
||||
&__title {
|
||||
background: var(--yj-surface-muted);
|
||||
color: var(--yj-ink);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 14px 20px;
|
||||
letter-spacing: 0.04em;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tp-section {
|
||||
min-width: 200px;
|
||||
flex: 1 1 200px;
|
||||
border-right: 1px solid var(--yj-line);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
background: var(--yj-brand-soft);
|
||||
padding: 11px 14px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__title-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__parent {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--yj-muted);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--yj-ink);
|
||||
}
|
||||
|
||||
&__target {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.tp-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.tp-lbl {
|
||||
padding: 8px 12px;
|
||||
color: var(--yj-muted);
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tp-val {
|
||||
padding: 8px 14px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
color: var(--yj-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--total {
|
||||
color: var(--yj-accent);
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 目标完成率色条:目标看板(卡片)Tab 复用 */
|
||||
.tp-good {
|
||||
color: #15803d !important;
|
||||
font-weight: 700;
|
||||
@@ -4071,6 +4104,126 @@ onMounted(async () => {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 目标看板卡片栅格(与顶部「甄养堂」条带数据同源,8 列铺满) */
|
||||
.tp-matrix-wrap {
|
||||
min-height: 120px;
|
||||
padding: 8px 4px 16px;
|
||||
}
|
||||
|
||||
.tp-matrix__title {
|
||||
margin: 0 0 18px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-ink);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.tp-matrix {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
.tp-matrix {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.tp-matrix {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.tp-matrix-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--yj-line);
|
||||
border-radius: var(--yj-radius-sm);
|
||||
background: var(--yj-surface);
|
||||
box-shadow: var(--yj-shadow-sm);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--yj-brand-soft);
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
}
|
||||
|
||||
&__titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__parent {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--yj-muted);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: var(--yj-ink);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
&__target {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--yj-accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
&__lbl {
|
||||
padding: 7px 10px;
|
||||
color: var(--yj-muted);
|
||||
font-size: 11px;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__num {
|
||||
padding: 7px 10px;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--yj-line);
|
||||
color: var(--yj-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&--hot {
|
||||
color: var(--yj-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tp-matrix-empty {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.leaderboard-sub {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -4640,6 +4793,10 @@ tbody tr.yeji-row--action:hover .yeji-lead-cell--link {
|
||||
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||
}
|
||||
|
||||
tbody tr.total-row:hover .yeji-lead-cell--link {
|
||||
color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000);
|
||||
}
|
||||
|
||||
.yeji-leadlines-dialog__pager {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
v-model="form.channel_source"
|
||||
placeholder="请选择渠道来源"
|
||||
class="channel-source-select"
|
||||
@change="onChannelSourceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
@@ -58,6 +59,20 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="needsChannelSourceDetail"
|
||||
label="自媒体补充:"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="form.channel_source_detail"
|
||||
clearable
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入自媒体相关补充内容"
|
||||
class="channel-source-select"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 预约医生 -->
|
||||
<el-form-item label="预约医生:">
|
||||
<div class="doctor-list">
|
||||
@@ -233,9 +248,44 @@ const form = reactive({
|
||||
date: '',
|
||||
appointmentTime: '',
|
||||
remark: '',
|
||||
channel_source: '' as string
|
||||
channel_source: '' as string,
|
||||
channel_source_detail: '' as string
|
||||
})
|
||||
|
||||
/** 仅这些渠道字典 name 需填「自媒体补充」(与后台字典名称完全一致) */
|
||||
const CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL = new Set([
|
||||
'自媒体4H',
|
||||
'自媒体3Q',
|
||||
'自媒体3H',
|
||||
'自媒体2H',
|
||||
'自媒体2Q'
|
||||
])
|
||||
|
||||
function channelNameRequiresSelfMediaDetail(name: string) {
|
||||
return CHANNEL_NAMES_REQUIRING_SELF_MEDIA_DETAIL.has(String(name ?? '').trim())
|
||||
}
|
||||
|
||||
/** 当前选中渠道字典名称(用于判断是否需填「自媒体补充」) */
|
||||
const selectedChannelDictName = computed(() => {
|
||||
const v = form.channel_source
|
||||
if (v === '' || v == null) return ''
|
||||
const row = channelOptions.value.find((item: any) => String(item.value) === String(v))
|
||||
return row ? String(row.name ?? '').trim() : ''
|
||||
})
|
||||
|
||||
/** 仅白名单内字典项需填写补充说明 */
|
||||
const needsChannelSourceDetail = computed(() =>
|
||||
channelNameRequiresSelfMediaDetail(selectedChannelDictName.value)
|
||||
)
|
||||
|
||||
const onChannelSourceChange = (val: string) => {
|
||||
const row = channelOptions.value.find((item: any) => String(item.value) === String(val))
|
||||
const name = row ? String(row.name ?? '').trim() : ''
|
||||
if (!channelNameRequiresSelfMediaDetail(name)) {
|
||||
form.channel_source_detail = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 生成未来7天的日期选项(只显示有排班的日期,且大于等于今天)
|
||||
const dateOptions = computed<DateOption[]>(() => {
|
||||
if (!selectedDoctorId.value || doctorRosterDates.value.length === 0) {
|
||||
@@ -321,7 +371,8 @@ const canSubmit = computed(() => {
|
||||
selectedDoctorId.value &&
|
||||
form.date &&
|
||||
form.appointmentTime &&
|
||||
!!form.channel_source
|
||||
!!form.channel_source &&
|
||||
(!needsChannelSourceDetail.value || form.channel_source_detail.trim() !== '')
|
||||
)
|
||||
})
|
||||
|
||||
@@ -344,6 +395,7 @@ const open = async (patient: any) => {
|
||||
form.appointmentTime = ''
|
||||
form.remark = ''
|
||||
form.channel_source = ''
|
||||
form.channel_source_detail = ''
|
||||
timeSlots.value = []
|
||||
doctorRosterDates.value = []
|
||||
lastVisit.value = ''
|
||||
@@ -412,7 +464,13 @@ const loadLastVisit = async () => {
|
||||
const loadChannelOptions = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
channelOptions.value = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
const rows = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
rows.sort((a: any, b: any) => {
|
||||
const ds = Number(b?.sort ?? 0) - Number(a?.sort ?? 0)
|
||||
if (ds !== 0) return ds
|
||||
return Number(b?.id ?? 0) - Number(a?.id ?? 0)
|
||||
})
|
||||
channelOptions.value = rows
|
||||
} catch (e) {
|
||||
console.error('加载渠道来源失败:', e)
|
||||
channelOptions.value = []
|
||||
@@ -612,6 +670,10 @@ const handleConfirm = async () => {
|
||||
feedback.msgWarning('请选择渠道来源')
|
||||
return
|
||||
}
|
||||
if (needsChannelSourceDetail.value && !form.channel_source_detail.trim()) {
|
||||
feedback.msgWarning('请填写自媒体渠道补充信息')
|
||||
return
|
||||
}
|
||||
if (blockedByTodayDuplicate.value) {
|
||||
feedback.msgWarning(
|
||||
'该患者今日已有挂号(已预约或已过号),不能重复预约今天,请改选其他日期。'
|
||||
@@ -631,7 +693,8 @@ const handleConfirm = async () => {
|
||||
appointment_time: form.appointmentTime,
|
||||
appointment_type: form.appointmentType,
|
||||
remark: form.remark,
|
||||
channel_source: form.channel_source
|
||||
channel_source: form.channel_source,
|
||||
channel_source_detail: form.channel_source_detail.trim()
|
||||
}
|
||||
|
||||
console.log('提交预约参数:', params)
|
||||
|
||||
@@ -105,7 +105,9 @@ function channelLabel(row: Record<string, any>) {
|
||||
const raw = row.channel_source ?? row.channels ?? ''
|
||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||
const key = String(raw)
|
||||
return channelMap.value[key] || key
|
||||
const base = channelMap.value[key] || key
|
||||
const extra = String(row.channel_source_detail ?? '').trim()
|
||||
return extra !== '' ? `${base}(${extra})` : base
|
||||
}
|
||||
|
||||
function sortRows(list: any[]) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -552,7 +552,7 @@ class PrescriptionOrderLists extends BaseAdminDataLists implements ListsSearchIn
|
||||
unset($item);
|
||||
|
||||
if ((int) ($this->params['yeji_order_drawer'] ?? 0) === 1) {
|
||||
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists);
|
||||
PrescriptionOrderLogic::appendLinkedAppointmentsToListRows($lists, $this->params);
|
||||
}
|
||||
|
||||
return $lists;
|
||||
|
||||
@@ -28,6 +28,20 @@ use think\facade\Log;
|
||||
*/
|
||||
class AppointmentLogic extends BaseLogic
|
||||
{
|
||||
/** 仅这些字典名称需填写 channel_source_detail(与 admin 预约弹窗白名单一致,按 name 全等) */
|
||||
private const CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL = [
|
||||
'自媒体4H',
|
||||
'自媒体3Q',
|
||||
'自媒体3H',
|
||||
'自媒体2H',
|
||||
'自媒体2Q',
|
||||
];
|
||||
|
||||
private static function channelDictNameRequiresSourceDetail(string $name): bool
|
||||
{
|
||||
return in_array($name, self::CHANNEL_NAMES_REQUIRING_SOURCE_DETAIL, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取可用时间段
|
||||
* @param array $params
|
||||
@@ -233,6 +247,21 @@ class AppointmentLogic extends BaseLogic
|
||||
Db::rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
$chSrc = trim((string) ($params['channel_source'] ?? ''));
|
||||
$chDetail = trim((string) ($params['channel_source_detail'] ?? ''));
|
||||
$channelDictName = trim((string) (DictData::where('type_value', 'channels')
|
||||
->where('value', $chSrc)
|
||||
->value('name') ?? ''));
|
||||
if ($channelDictName !== '' && self::channelDictNameRequiresSourceDetail($channelDictName)) {
|
||||
if ($chDetail === '') {
|
||||
self::setError('请填写自媒体渠道补充信息');
|
||||
Db::rollback();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'patient_id' => $params['patient_id'],
|
||||
'assistant_id'=>$params['assistant_id'],
|
||||
@@ -244,7 +273,8 @@ class AppointmentLogic extends BaseLogic
|
||||
'appointment_type' => $params['appointment_type'] ?? 'video',
|
||||
'remark' => $params['remark'] ?? '',
|
||||
// 表字段为 channel_source(与字典 type_value=channels 的 value 一致);勿再写入不存在的 channels 列
|
||||
'channel_source' => trim((string) ($params['channel_source'] ?? '')),
|
||||
'channel_source' => $chSrc,
|
||||
'channel_source_detail' => $chDetail,
|
||||
'status' => 1,
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
|
||||
@@ -4701,7 +4701,8 @@ class YejiStatsLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source)
|
||||
* 业绩看板业务订单侧栏:按 channel_code 收窄(与 overview「{渠道}业绩」同患者判定;含 ap.channels 与 ap.channel_source);
|
||||
* **二中心及其组织下级**展示部门与看板一致:选定渠道收窄时本条无渠道订单(表中渠道业绩已为 0)。
|
||||
*
|
||||
* @param Query $query PrescriptionOrder 主表查询
|
||||
* @param array<string, mixed> $params
|
||||
@@ -4715,6 +4716,16 @@ class YejiStatsLogic
|
||||
if ($channelCode === '') {
|
||||
return;
|
||||
}
|
||||
/** @see excludeErCenterSubtreeFromChannelMetrics 看板选定渠道时对二中心子树渠道列置 0,侧栏须同步 */
|
||||
$assistantDeptForDrawer = (int) ($params['assistant_dept_id'] ?? 0);
|
||||
if ($assistantDeptForDrawer > 0) {
|
||||
$erCenterSubtree = DeptLogic::getErCenterSubtreeDeptIdSet();
|
||||
if (isset($erCenterSubtree[$assistantDeptForDrawer])) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
$startDate = '';
|
||||
$endDate = '';
|
||||
$st = trim((string) ($params['start_time'] ?? ''));
|
||||
@@ -4803,4 +4814,110 @@ class YejiStatsLogic
|
||||
$query->whereRaw('0 = 1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 业绩看板业务订单侧栏:在「渠道筛选」模式下,为每个诊单挑一条用于「关联挂号」展示的预约行——
|
||||
* 须为已完成(status=3)、挂号助理为医助(role_id=2),且 channels/channel_source 与当前 channel_code 同源命中(与 {@see applyYejiDrawerChannelFilterToPrescriptionOrderQuery} 的 dict 收窄一致)。
|
||||
* 若为 tag 等未走 channels 表达式的情况则返回空图,交由列表沿用处方/最近挂号。
|
||||
*
|
||||
* @param int[] $diagnosisIds doctor_appointment.patient_id(与本系统诊单 id 一致)
|
||||
* @param array<string, mixed> $params 与列表同源:yeji_order_drawer、channel_code、start_time、end_time
|
||||
*
|
||||
* @return array<int, int> diagnosis_id => appointment_id
|
||||
*/
|
||||
public static function batchResolveYejiChannelDrawerHighlightAppointmentIds(array $diagnosisIds, array $params): array
|
||||
{
|
||||
$diagnosisIds = array_values(array_unique(array_filter(array_map('intval', $diagnosisIds), static fn (int $x): bool => $x > 0)));
|
||||
if ($diagnosisIds === []) {
|
||||
return [];
|
||||
}
|
||||
if ((int) ($params['yeji_order_drawer'] ?? 0) !== 1) {
|
||||
return [];
|
||||
}
|
||||
$channelCode = trim((string) ($params['channel_code'] ?? ''));
|
||||
if ($channelCode === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$startDate = '';
|
||||
$endDate = '';
|
||||
$st = trim((string) ($params['start_time'] ?? ''));
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $st, $m)) {
|
||||
$startDate = $m[1];
|
||||
}
|
||||
$et = trim((string) ($params['end_time'] ?? ''));
|
||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $et, $m)) {
|
||||
$endDate = $m[1];
|
||||
}
|
||||
if ($startDate === '' || $endDate === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ctx = self::resolveYejiContext([
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'channel_code' => $channelCode,
|
||||
'dept_ids' => null,
|
||||
]);
|
||||
if (!$ctx['channelFilterActive'] || $ctx['channelInfo'] === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$norm = self::normalizeAppointmentChannelValues($ctx['appointmentChannelValues']);
|
||||
$srcStrings = self::appointmentChannelDictStringValuesForDrawer($ctx['channelInfo']);
|
||||
if ($norm === [] && $srcStrings === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$apTable = self::tableWithPrefix('doctor_appointment');
|
||||
$arTable = self::tableWithPrefix('admin_role');
|
||||
|
||||
$hasCh = self::doctorAppointmentHasChannelsColumn();
|
||||
$orParts = [];
|
||||
$channelBind = [];
|
||||
if ($norm !== [] && $hasCh) {
|
||||
$strVals = array_values(array_unique(array_map(static fn (int $v): string => (string) $v, $norm)));
|
||||
$ph = implode(',', array_fill(0, count($strVals), '?'));
|
||||
$orParts[] = "ap.`channels` IN ({$ph})";
|
||||
foreach ($strVals as $sv) {
|
||||
$channelBind[] = $sv;
|
||||
}
|
||||
}
|
||||
if ($srcStrings !== [] && self::doctorAppointmentHasChannelSourceColumn()) {
|
||||
$ph = implode(',', array_fill(0, count($srcStrings), '?'));
|
||||
$orParts[] = "ap.`channel_source` IN ({$ph})";
|
||||
foreach ($srcStrings as $s) {
|
||||
$channelBind[] = $s;
|
||||
}
|
||||
}
|
||||
if ($orParts === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$inPh = implode(',', array_fill(0, count($diagnosisIds), '?'));
|
||||
$bind = array_merge($diagnosisIds, $channelBind);
|
||||
|
||||
$orSql = '(' . implode(' OR ', $orParts) . ')';
|
||||
$sql = "SELECT ap.`patient_id` AS dg, MAX(ap.`id`) AS aid FROM `{$apTable}` ap "
|
||||
. "INNER JOIN `{$arTable}` ar ON ar.`admin_id` = ap.`assistant_id` AND ar.`role_id` = 2 "
|
||||
. "WHERE ap.`patient_id` IN ({$inPh}) AND ap.`status` = 3 AND {$orSql} "
|
||||
. 'GROUP BY ap.`patient_id`';
|
||||
|
||||
try {
|
||||
$rows = Db::query($sql, $bind);
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$dg = (int) ($r['dg'] ?? $r['DG'] ?? 0);
|
||||
$aid = (int) ($r['aid'] ?? $r['AID'] ?? 0);
|
||||
if ($dg > 0 && $aid > 0) {
|
||||
$out[$dg] = $aid;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\tcm;
|
||||
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\order\OrderLogic;
|
||||
use app\common\model\Order;
|
||||
@@ -1049,11 +1050,13 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约)
|
||||
* 业务订单列表行批量附加 linked_appointment(解析规则与详情一致:优先处方 appointment_id,否则诊单最近一条预约;
|
||||
* 业绩侧栏且带 channel_code 时优先展示与渠道收窄同源的已完成挂号,避免列表行属自媒体2Q 但展示关联为自媒体1)
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $lists
|
||||
* @param array<string, mixed> $listsParams PrescriptionOrderLists 请求参数(可选)
|
||||
*/
|
||||
public static function appendLinkedAppointmentsToListRows(array &$lists): void
|
||||
public static function appendLinkedAppointmentsToListRows(array &$lists, array $listsParams = []): void
|
||||
{
|
||||
if ($lists === []) {
|
||||
return;
|
||||
@@ -1063,6 +1066,23 @@ class PrescriptionOrderLogic
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$yejiChannelHighlightByDiag = [];
|
||||
if (
|
||||
(int) ($listsParams['yeji_order_drawer'] ?? 0) === 1
|
||||
&& trim((string) ($listsParams['channel_code'] ?? '')) !== ''
|
||||
) {
|
||||
$allDiagIdsForYeji = array_values(array_unique(array_filter(
|
||||
array_map(static fn ($x): int => (int) ($x['diagnosis_id'] ?? 0), $lists),
|
||||
static fn (int $id): bool => $id > 0
|
||||
)));
|
||||
if ($allDiagIdsForYeji !== []) {
|
||||
$yejiChannelHighlightByDiag = YejiStatsLogic::batchResolveYejiChannelDrawerHighlightAppointmentIds(
|
||||
$allDiagIdsForYeji,
|
||||
$listsParams
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$rxIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($x) => (int) ($x['prescription_id'] ?? 0),
|
||||
$lists
|
||||
@@ -1113,6 +1133,10 @@ class PrescriptionOrderLogic
|
||||
if ($resolved <= 0 && $dgId > 0) {
|
||||
$resolved = (int) ($latestApptByDiag[$dgId] ?? 0);
|
||||
}
|
||||
$preferredYeji = $dgId > 0 ? (int) ($yejiChannelHighlightByDiag[$dgId] ?? 0) : 0;
|
||||
if ($preferredYeji > 0) {
|
||||
$resolved = $preferredYeji;
|
||||
}
|
||||
if ($resolved > 0) {
|
||||
$resolvedPairs[$idx] = [$resolved, $rxApptId];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class AppointmentValidate extends BaseValidate
|
||||
'appointment_time' => 'require',
|
||||
'appointment_type' => 'in:video,text,phone',
|
||||
'channel_source' => 'require',
|
||||
'channel_source_detail' => 'max:128',
|
||||
'note_id' => 'require|integer',
|
||||
'image_type' => 'require|in:tongue_images,report_files',
|
||||
'image_path' => 'require',
|
||||
@@ -44,6 +45,7 @@ class AppointmentValidate extends BaseValidate
|
||||
'appointment_time' => '预约时间',
|
||||
'appointment_type' => '预约类型',
|
||||
'channel_source' => '渠道来源',
|
||||
'channel_source_detail' => '渠道补充说明',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -52,7 +54,7 @@ class AppointmentValidate extends BaseValidate
|
||||
*/
|
||||
public function sceneCreate()
|
||||
{
|
||||
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source']);
|
||||
return $this->only(['patient_id', 'doctor_id', 'appointment_date', 'period', 'appointment_time', 'appointment_type', 'channel_source', 'channel_source_detail']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 挂号:自媒体等渠道所需的补充说明(与 channel_source 配合使用)
|
||||
ALTER TABLE `zyt_doctor_appointment`
|
||||
ADD COLUMN `channel_source_detail` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道补充说明(如自媒体账号等)' AFTER `channel_source`;
|
||||
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `zyt_doctor_appointment` (
|
||||
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1=已预约,2=已取消,3=已完成',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`channel_source` varchar(64) NOT NULL DEFAULT '' COMMENT '渠道来源(字典channels)',
|
||||
`channel_source_detail` varchar(128) NOT NULL DEFAULT '' COMMENT '渠道补充说明(如自媒体账号等)',
|
||||
`create_time` int(10) unsigned DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` int(10) unsigned DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
|
||||
Reference in New Issue
Block a user