- 当月被指派总数:当月内诊单被指派给医助(按指派操作时间落月,剔除勾选「继承」的指派)的诊单数,按「医助 × 诊单」去重;部门行 / 合计行按诊单去重;再剔除名下存在履约「拒收 / 退款」业务订单的诊单。
+ {{ isSlotMode ? '新指派数' : '当月被指派数' }}:当月内诊单被指派给医助(按指派操作时间落月,不含勾选「继承」的指派)的诊单数,按「医助 × 诊单」去重;部门行 / 合计行按诊单去重;再剔除名下存在履约「拒收 / 退款」业务订单的诊单。与业绩看板「被指派数」同口径。
+
+
+
+ 继承数:当月勾选「继承」转接给该医助的诊单数(同一诊单多次继承取最早;同月既新指派又继承给同一医助只算新指派;历史「回退自动分配」行按继承处理;指派给自己的记录忽略)。继承时诊次 k = 继承时刻之前患者已完成的诊次(有效实单数 + 偏移),起算诊次 = max(2, k+1):继承者只对第 k+1 诊及以后负责。
+
+
+ N 诊分母 = 新指派数 + 起算诊次 ≤ N 的继承数(每列独立);N 诊接诊率 = N 诊单数 ÷ N 诊分母,分母为 0 显示「—」。复诊合计:单数 = 各诊次单数之和,分母 = 新指派 ∪ 继承(被指派合计),比率可超过 100%(人均复诊成交单数)。
+
+
+ 继承分母依赖指派记录的「继承」标记(2026-06 起写入),之前的转接无法回溯;切换为「旧口径」可对照上线前数字。
+
+
+
+
+ 旧口径:继承指派不计入分母,N 诊分母 = 新指派数(对所有 N 相同),新指派数 / N 诊单数 / N 诊接诊率与 2026-10 之前完全一致;继承数恒为 0。切换为「新口径」后继承按继承时诊次计入每列独立分母。
+
+
+
+ 诊次(第 N 次下单):患者(诊单)名下计入业绩的业务订单(剔除已取消 / 拒收 / 退款)按下单时间升序编号为「实单序号」,统计诊次 = 实单序号 + 诊单偏移(默认偏移 0 → 第 1 笔实单为一诊)。诊次跨月累计不重置。诊单可在「业务订单」tab 配置偏移量。
- 诊次(第 N 次下单):患者(诊单)名下计入业绩的业务订单(剔除已取消 / 拒收 / 退款)按下单时间升序编号为「实单序号」,统计诊次 = 实单序号 + 诊单偏移(默认偏移 0 → 第 1 笔实单为一诊;偏移 1 → 第 1 笔实单为二诊;偏移 2 → 第 1 笔实单为三诊,5 笔实单等价七诊)。诊次跨月累计不重置。诊单可在「业务订单」tab 配置偏移量。
-
-
- 当月 N 诊单数:当月内下单且诊次为 N 的订单数,归属下单时点持有该患者的医助(指派可在往月;释放后不再归属;「继承」指派会转移持有人但不计被指派数)。
-
-
- 当月 N 诊接诊率 = 当月 N 诊单数 ÷ 当月被指派总数。往月指派、当月成交会推高分子,比率可能超过 100%;医助当月无新指派但旗下有成交时,被指派数为 0、比率显示「—」。
+ 当月 N 诊单数:当月内下单且诊次为 N 的订单数,归属下单时点持有该患者的医助(指派可在往月;释放后不再归属;继承会转移持有人)。往月指派、当月成交会推高分子,比率可能超过 100%。
医助按人事部门归组;仅统计「二中心」及其组织下级;部门下拉与未选时的默认范围均限定在该子树,选定部门时含其组织下级。
@@ -81,7 +100,9 @@
{{ row.assistant_name }}
-
+
+
+ {{ isSlotMode ? '新指派数' : '当月被指派数' }}
+
+
+
+
{{ row.assigned_count }}
{{ row.assigned_count }}
+
+
+ 继承数
+
+
+
+
+
+
+ {{ row.inherit_count }}
+
+ {{ row.inherit_count ?? 0 }}
+
+
+
+
+ 被指派合计
+
+
+
+
+
+
+ {{ row.assigned_total_count }}
+
+ {{ row.assigned_total_count ?? 0 }}
+
+
- {{ formatRate(row[`visit${slot}_rate`]) }}
+
+ {{ slotLabel(slot) }}接诊率
+
+
+
+
+
+
+ {{ formatRate(row[`visit${slot}_rate`]) }}
+ {{ denText(row, `visit${slot}`) }}
+
+
+
+
+ 复诊合计单数
+
+
+
+
+
+
+ {{ row.visit_all_count }}
+
+ {{ row.visit_all_count ?? 0 }}
+
+
+
+
+ 复诊合计接诊率
+
+
+
+
+
+
+ {{ formatRate(row.visit_all_rate) }}
+ {{ denText(row, 'visit_all') }}
+
+
+
@@ -162,7 +304,7 @@
@@ -193,6 +335,33 @@
{{ row.patient_phone || '—' }}
+
+
+
+ {{ row.assign_type_text || (row.assign_type === 'inherit' ? '继承' : '新指派') }}
+
+
+
+
+
+ 继承时诊次
+
+
+
+
+
+ {{ row.assign_type === 'inherit' ? `第 ${Number(row.inherit_visit_slot ?? 0)} 诊` : '—' }}
+
+
+
+
+ 起算诊次
+
+
+
+
+ {{ entrySlotText(row.entry_slot) }}
+
@@ -210,6 +379,9 @@
{{ row.order_no || `#${row.order_id}` }}
+
+ {{ slotLabel(Number(row.visit_slot)) }}
+
@@ -251,7 +423,9 @@ import {
revisitRateAssignLines,
revisitRateDeptOptions,
revisitRateOverview,
- revisitRateVisitOrderLines
+ revisitRateVisitOrderLines,
+ type RevisitRateAssignKind,
+ type RevisitRateInheritMode
} from '@/api/stats'
import { hasPermission } from '@/utils/perm'
@@ -263,7 +437,14 @@ const OrderDetailDrawer = defineAsyncComponent(
const DiagnosisEditPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
interface MetricPack extends Record {
+ /** 新指派数(语义不变) */
assigned_count: number
+ inherit_count?: number
+ assigned_total_count?: number
+ visit_all_count?: number
+ visit_all_den?: number
+ visit_all_rate?: number | null
+ visit2_rate_new_assign?: number | null
}
interface StatsRow extends MetricPack {
@@ -289,11 +470,14 @@ const canViewOrderLines = hasPermission(['stats.revisitRate/visitOrderLines'])
const canViewOrderDetail = hasPermission(['tcm.prescriptionOrder/detail'])
const canViewPatientDetail = hasPermission(['tcm.diagnosis/detail'])
-/** 接口动态返回的诊次分档(最少 2~4,按数据扩展到五诊、六诊…) */
+/** 接口动态返回的诊次分档(最少 2~4,按分子最大诊次 / 最大起算诊次扩展到五诊、六诊…) */
const visitSlots = ref([2, 3, 4])
const cnDigits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
const slotLabel = (slot: number) => {
+ if (slot === 0) {
+ return '复诊合计'
+ }
if (slot >= 2 && slot <= 10) {
return `${cnDigits[slot]}诊`
}
@@ -307,6 +491,8 @@ const initNow = new Date()
const currentMonth = `${initNow.getFullYear()}-${String(initNow.getMonth() + 1).padStart(2, '0')}`
const month = ref(currentMonth)
const deptId = ref(undefined)
+/** 口径:首次加载不传、取后端默认(config default_inherit_mode),之后以用户选择为准 */
+const inheritMode = ref('')
const loading = ref(false)
const rows = ref([])
const total = ref(null)
@@ -314,6 +500,8 @@ const total = ref(null)
const deptTreeOptions = ref([])
const deptTreeProps = { value: 'id', label: 'name', children: 'children' }
+const isSlotMode = computed(() => inheritMode.value !== 'exclude')
+
const monthText = computed(() => {
const [y, m] = month.value.split('-')
return y && m ? `${y}年${Number(m)}月` : month.value
@@ -322,9 +510,30 @@ const monthText = computed(() => {
const formatRate = (rate: number | null | undefined) =>
rate === null || rate === undefined ? '—' : `${rate}%`
+/** 接诊率单元格下方灰字:单数 / 分母 */
+const denText = (row: Record, prefix: string) => {
+ const cnt = Number(row[`${prefix}_count`] ?? 0)
+ const den = row[`${prefix}_den`]
+ return `${cnt} / ${den === null || den === undefined ? '—' : den}`
+}
+
+const rateHeaderTip = (slot: number) =>
+ isSlotMode.value
+ ? `${slotLabel(slot)}单数 ÷ ${slotLabel(slot)}分母;分母 = 当月新指派数 + 起算诊次 ≤ ${slot} 的继承数;分母为 0 显示「—」`
+ : `${slotLabel(slot)}单数 ÷ 当月新指派数(旧口径:继承不计分母);分母为 0 显示「—」`
+
+const entrySlotText = (entry: unknown) => {
+ const n = Number(entry ?? 0)
+ if (n <= 0) {
+ return '—'
+ }
+ return n > 50 ? `${n}(仅计合计)` : slotLabel(n)
+}
+
const baseQuery = () => ({
month: month.value,
- dept_ids: deptId.value ? String(deptId.value) : ''
+ dept_ids: deptId.value ? String(deptId.value) : '',
+ inherit_mode: (inheritMode.value || undefined) as RevisitRateInheritMode | undefined
})
interface SummaryCard {
@@ -341,11 +550,11 @@ const summaryCards = computed(() => {
const cards: SummaryCard[] = [
{
key: 'assigned',
- label: '当月被指派总数',
+ label: isSlotMode.value ? '当月新指派数' : '当月被指派数',
value: t ? String(t.assigned_count) : '—',
- sub: '',
+ sub: t && isSlotMode.value ? `继承 ${t.inherit_count ?? 0} · 被指派合计 ${t.assigned_total_count ?? 0}` : '',
clickable: !!(canViewAssignLines && t && t.assigned_count > 0),
- onClick: () => openAssignDetail(null)
+ onClick: () => openAssignDetail(null, 'new')
}
]
for (const slot of visitSlots.value) {
@@ -355,11 +564,19 @@ const summaryCards = computed(() => {
key: `visit${slot}`,
label: `当月${slotLabel(slot)}接诊率`,
value: t ? formatRate(rate) : '—',
- sub: t ? `${slotLabel(slot)}下单 ${cnt} 单` : '',
+ sub: t ? `${slotLabel(slot)}下单 ${cnt} 单 / 分母 ${t[`visit${slot}_den`] ?? '—'}` : '',
clickable: !!(canViewOrderLines && cnt > 0),
onClick: () => openOrderDetail(slot, null)
})
}
+ cards.push({
+ key: 'visit_all',
+ label: '当月复诊合计接诊率',
+ value: t ? formatRate(t.visit_all_rate) : '—',
+ sub: t ? `复诊下单 ${t.visit_all_count ?? 0} 单 / 被指派合计 ${t.visit_all_den ?? '—'}` : '',
+ clickable: !!(canViewOrderLines && t && Number(t.visit_all_count) > 0),
+ onClick: () => openOrderDetail(0, null)
+ })
return cards
})
@@ -374,15 +591,16 @@ const getSummaries = ({ columns }: { columns: any[] }) => {
return ''
}
const prop = String(col.property || '')
- if (prop === 'assigned_count') {
- return String(t.assigned_count)
+ if (prop === 'assigned_count' || prop === 'inherit_count' || prop === 'assigned_total_count') {
+ return String(t[prop] ?? 0)
}
- if (/^visit\d+_count$/.test(prop)) {
+ const m = /^visit(\d+|all)_(count|den|rate)$/.exec(prop)
+ if (m) {
+ if (m[2] === 'rate') {
+ return `${formatRate(t[prop])}(${denText(t, `visit${m[1]}`)})`
+ }
return String(t[prop] ?? '')
}
- if (/^visit\d+_rate$/.test(prop)) {
- return formatRate(t[prop])
- }
return ''
})
}
@@ -404,6 +622,9 @@ const loadData = async () => {
if (res?.month) {
month.value = res.month
}
+ if (res?.inherit_mode === 'slot' || res?.inherit_mode === 'exclude') {
+ inheritMode.value = res.inherit_mode
+ }
} finally {
loading.value = false
}
@@ -449,6 +670,8 @@ const detailType = ref<'assign' | 'orders'>('assign')
const detailTitle = ref('')
const detailLoading = ref(false)
const detailRows = ref([])
+/** 订单明细的诊次(0 = 复诊合计,表格多显示「诊次」列) */
+const detailSlot = ref(2)
/** row 为 null 时表示合计(当前部门筛选范围) */
const rowScope = (row: StatsRow | null): { assistant_id?: number; dept_id?: number; label: string } => {
@@ -461,13 +684,19 @@ const rowScope = (row: StatsRow | null): { assistant_id?: number; dept_id?: numb
return { assistant_id: row.assistant_id, label: row.assistant_name || '' }
}
-const openAssignDetail = async (row: StatsRow | null) => {
+const assignKindLabel: Record = {
+ new: '新指派明细',
+ inherit: '继承明细',
+ all: '被指派合计明细'
+}
+
+const openAssignDetail = async (row: StatsRow | null, kind: RevisitRateAssignKind = 'all') => {
if (!canViewAssignLines) {
return
}
const scope = rowScope(row)
detailType.value = 'assign'
- detailTitle.value = `${scope.label} · 被指派明细(${monthText.value})`
+ detailTitle.value = `${scope.label} · ${assignKindLabel[kind]}(${monthText.value})`
detailVisible.value = true
detailLoading.value = true
detailRows.value = []
@@ -475,7 +704,8 @@ const openAssignDetail = async (row: StatsRow | null) => {
const res = await revisitRateAssignLines({
...baseQuery(),
assistant_id: scope.assistant_id,
- dept_id: scope.dept_id
+ dept_id: scope.dept_id,
+ kind
})
detailRows.value = res?.rows ?? []
} finally {
@@ -483,12 +713,14 @@ const openAssignDetail = async (row: StatsRow | null) => {
}
}
+/** slot=0 表示全部诊次 ≥ 2(复诊合计单数) */
const openOrderDetail = async (slot: number, row: StatsRow | null) => {
if (!canViewOrderLines) {
return
}
const scope = rowScope(row)
detailType.value = 'orders'
+ detailSlot.value = slot
detailTitle.value = `${scope.label} · ${slotLabel(slot)}订单明细(${monthText.value})`
detailVisible.value = true
detailLoading.value = true
@@ -529,6 +761,7 @@ const openPatientDetail = (diagnosisId: number) => {
const handleReset = () => {
month.value = currentMonth
deptId.value = undefined
+ inheritMode.value = ''
loadData()
}
@@ -623,6 +856,22 @@ onMounted(() => {
color: var(--el-text-color-secondary);
cursor: help;
}
+
+ .rate-cell {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ line-height: 1.3;
+
+ .rate-cell-main {
+ font-weight: 500;
+ }
+
+ .rate-cell-sub {
+ font-size: 11px;
+ color: var(--el-text-color-placeholder);
+ }
+ }
}
.rate-detail-footer {
@@ -640,5 +889,10 @@ onMounted(() => {
p + p {
margin-top: 6px;
}
+
+ .rate-caliber-note {
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+ }
}
diff --git a/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue b/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
index 892b96a2d..9bbd3ef5b 100644
--- a/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
+++ b/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue
@@ -12,9 +12,21 @@
{{ assistantLogCellText(row, 'to') }}
-
+
+
+ 继承
+
+
+
+
- 是
+
+ 是
+ {{ inheritSlotText(row) }}
+
否
@@ -85,6 +97,20 @@ function relatedPoCreateTimeCell(row: Record): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
+/** 对应日志表 inherit_visit_slot / inherit_visit_slot_calc_time:后端已拼好「继承时第 k 诊」文案,缺失时按快照列兜底 */
+function inheritSlotText(row: Record): string {
+ const text = row.inherit_visit_slot_text
+ if (text != null && String(text).trim() !== '') {
+ return String(text)
+ }
+ const calcTime = Number(row.inherit_visit_slot_calc_time)
+ if (!Number.isFinite(calcTime) || calcTime <= 0) {
+ return ''
+ }
+ const k = Number(row.inherit_visit_slot)
+ return Number.isFinite(k) && k > 0 ? `继承时第 ${k} 诊` : '继承时未成交'
+}
+
function assistantLogCellText(row: Record, side: 'from' | 'to'): string {
const nameKey = side === 'from' ? 'from_assistant_name' : 'to_assistant_name'
const idKey = side === 'from' ? 'from_assistant_id' : 'to_assistant_id'
@@ -132,4 +158,11 @@ defineExpose({ refresh: load })
color: var(--el-text-color-secondary);
cursor: help;
}
+
+.assign-log-inherit-slot {
+ margin-top: 2px;
+ font-size: 12px;
+ line-height: 1.3;
+ color: var(--el-text-color-secondary);
+}
diff --git a/admin/src/views/tcm/diagnosis/components/RevisitNodeCell.vue b/admin/src/views/tcm/diagnosis/components/RevisitNodeCell.vue
new file mode 100644
index 000000000..b4ab5aff9
--- /dev/null
+++ b/admin/src/views/tcm/diagnosis/components/RevisitNodeCell.vue
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+ {{ slotText }}
+
+
+
+ {{ slotText }}
+
+
+ {{ prescribedLabel }}
+
+
+ ·{{ followupDoctorName }}
+
+
+
+
+
+
+ {{ orderDateText }} 开药
+ ·
+ {{ medicationText }}
+
+ ·
+ {{ anchorText }}
+
+
+
+
+
+ {{ dueDateText }}
+
+ {{ chipText }}
+
+
+
+
+ {{ followLineText }}
+
+
—
+
+
+
+
+
+
diff --git a/admin/src/views/tcm/diagnosis/index.vue b/admin/src/views/tcm/diagnosis/index.vue
index 7a1c189ed..f5916ee13 100644
--- a/admin/src/views/tcm/diagnosis/index.vue
+++ b/admin/src/views/tcm/diagnosis/index.vue
@@ -14,6 +14,7 @@
formData.pending_assign !== '1' &&
formData.pending_booking !== '1' &&
formData.completed_appointment !== '1' &&
+ formData.revisit_state === '' &&
!hasLatestAppointmentFilter() &&
formData.appointment_date === tab.value
}
@@ -30,6 +31,14 @@
待预约
{{ pendingBookingCount }}
+
+
+ 复诊到期
+ {{ revisitDueCount }}
+
+
+
+
+
+
+
重置
@@ -278,22 +317,20 @@