更新
This commit is contained in:
@@ -12,9 +12,21 @@
|
||||
{{ assistantLogCellText(row, 'to') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="继承" width="72" align="center">
|
||||
<el-table-column label="继承" width="118" align="center">
|
||||
<template #header>
|
||||
<span>继承</span>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
content="继承时第 k 诊:继承时刻之前患者已完成的诊次(有效实单数 + 偏移),复诊接诊率从第 k+1 诊起计入新医助分母;2026-10 前的继承记录无此快照"
|
||||
>
|
||||
<el-icon class="assign-log-col-hint"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="Number(row.is_inherit) === 1" type="success" size="small">是</el-tag>
|
||||
<template v-if="Number(row.is_inherit) === 1">
|
||||
<el-tag type="success" size="small">是</el-tag>
|
||||
<div v-if="inheritSlotText(row)" class="assign-log-inherit-slot">{{ inheritSlotText(row) }}</div>
|
||||
</template>
|
||||
<span v-else class="text-gray-400">否</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -85,6 +97,20 @@ function relatedPoCreateTimeCell(row: Record<string, unknown>): 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, unknown>): 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<string, unknown>, 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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div :class="['rv-cell', { 'rv-cell--compact': compact }]">
|
||||
<!-- 行 1:[第 N 诊] tag + 已开方/未开方 + 复诊医生 -->
|
||||
<div class="rv-line rv-line--head">
|
||||
<el-tooltip
|
||||
v-if="showDue && tooltipLines.length"
|
||||
placement="top"
|
||||
:show-after="300"
|
||||
:hide-after="0"
|
||||
>
|
||||
<template #content>
|
||||
<div class="rv-tooltip">
|
||||
<div v-for="line in tooltipLines" :key="line">{{ line }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-tag size="small" :type="slotTagType" effect="light" class="rv-slot-tag">
|
||||
{{ slotText }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag
|
||||
v-else-if="showDue"
|
||||
size="small"
|
||||
:type="slotTagType"
|
||||
effect="light"
|
||||
class="rv-slot-tag"
|
||||
>
|
||||
{{ slotText }}
|
||||
</el-tag>
|
||||
<span
|
||||
v-if="prescribedLabel"
|
||||
:class="['rv-prescribed', hasPrescription ? 'rv-prescribed--yes' : 'rv-prescribed--no']"
|
||||
>
|
||||
{{ prescribedLabel }}
|
||||
</span>
|
||||
<span v-if="followupDoctorName" class="rv-doctor">
|
||||
<span v-if="prescribedLabel" class="rv-dot">·</span>{{ followupDoctorName }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="showDue">
|
||||
<!-- 行 2:最新开药日 · 开药 X 天 · 锚点来源;缺天数时橙色 -->
|
||||
<div class="rv-line rv-line--order">
|
||||
<span v-if="orderDateText" class="rv-order-date">{{ orderDateText }} 开药</span>
|
||||
<span v-if="orderDateText" class="rv-dot">·</span>
|
||||
<span :class="['rv-days', { 'rv-days--missing': daysMissing }]">{{ medicationText }}</span>
|
||||
<template v-if="!daysMissing && anchorText">
|
||||
<span class="rv-dot">·</span>
|
||||
<span class="rv-anchor">{{ anchorText }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 行 3:药尽 MM-DD + 状态 chip -->
|
||||
<div class="rv-line rv-line--due">
|
||||
<span v-if="dueDateText" class="rv-due-date">{{ dueDateText }}</span>
|
||||
<el-tag
|
||||
v-if="chipText"
|
||||
size="small"
|
||||
:type="chipType"
|
||||
:effect="chipEffect"
|
||||
:class="chipClass"
|
||||
>
|
||||
{{ chipText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 行 4:已延迟 N 次 · 已联系 约 MM-DD(有值才显示) -->
|
||||
<div v-if="followLineText" class="rv-line rv-line--follow">{{ followLineText }}</div>
|
||||
</template>
|
||||
<span v-else class="rv-none">—</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 复诊节点单元格(PC 列表 / H5 卡片紧凑 / 只读页 三处共用)
|
||||
*
|
||||
* 只负责展示,不发请求;数据来自列表行 `revisit_due`(契约 C),由父组件用
|
||||
* `normalizeRevisitDue` 规整后传入。state='none' 或 due 为 null 时仅显示行 1 + 「—」。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
formatRevisitMonthDay,
|
||||
hasRevisitDue,
|
||||
isRevisitDaysMissing,
|
||||
revisitAnchorSrcText,
|
||||
revisitDueDateText,
|
||||
revisitFollowLineText,
|
||||
revisitMedicationDaysText,
|
||||
revisitSlotText,
|
||||
revisitStateChipClass,
|
||||
revisitStateChipText,
|
||||
revisitStateTagEffect,
|
||||
revisitStateTagType,
|
||||
revisitTooltipLines,
|
||||
type RevisitDue
|
||||
} from '@/utils/revisitDue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 规整后的复诊节点对象;null / state='none' 显示「—」 */
|
||||
due: RevisitDue | null
|
||||
/** H5 紧凑模式:更小字号、更紧行距 */
|
||||
compact?: boolean
|
||||
/** 是否已开方(决定「已开方/未开方」默认文案与颜色) */
|
||||
hasPrescription?: boolean
|
||||
/** 复诊医生(原列表 followup_doctor_name) */
|
||||
followupDoctorName?: string
|
||||
/** 自定义「已开方/未开方」文案;传 '' 隐藏该段(H5 卡片头已有标签时用) */
|
||||
prescribedText?: string
|
||||
}>(),
|
||||
{
|
||||
compact: false,
|
||||
hasPrescription: false,
|
||||
followupDoctorName: ''
|
||||
}
|
||||
)
|
||||
|
||||
const showDue = computed(() => hasRevisitDue(props.due))
|
||||
|
||||
const prescribedLabel = computed(() => {
|
||||
if (props.prescribedText !== undefined) return props.prescribedText
|
||||
return props.hasPrescription ? '已开方' : '未开方'
|
||||
})
|
||||
|
||||
const slotText = computed(() => revisitSlotText(props.due))
|
||||
const slotTagType = computed<'primary' | 'info'>(() =>
|
||||
showDue.value && Number(props.due?.current_slot) > 0 ? 'primary' : 'info'
|
||||
)
|
||||
const tooltipLines = computed(() => revisitTooltipLines(props.due))
|
||||
|
||||
const orderDateText = computed(() => (showDue.value ? formatRevisitMonthDay(props.due?.order_date) : ''))
|
||||
const daysMissing = computed(() => isRevisitDaysMissing(props.due))
|
||||
const medicationText = computed(() => revisitMedicationDaysText(props.due))
|
||||
const anchorText = computed(() => (showDue.value ? revisitAnchorSrcText(props.due?.anchor_src) : ''))
|
||||
|
||||
const dueDateText = computed(() => revisitDueDateText(props.due))
|
||||
const chipText = computed(() => revisitStateChipText(props.due))
|
||||
const chipType = computed(() => revisitStateTagType(props.due?.state))
|
||||
const chipEffect = computed(() => revisitStateTagEffect(props.due?.state))
|
||||
const chipClass = computed(() => revisitStateChipClass(props.due))
|
||||
|
||||
const followLineText = computed(() => revisitFollowLineText(props.due))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rv-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rv-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rv-line--head {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.rv-slot-tag {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.rv-prescribed {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rv-prescribed--yes {
|
||||
color: #4a5dff;
|
||||
}
|
||||
|
||||
.rv-prescribed--no {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.rv-doctor {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rv-dot {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rv-line--order {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rv-days--missing {
|
||||
color: #ea580c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rv-anchor {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.rv-line--due {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rv-due-date {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 状态 chip:el-tag 基础色由 type/effect 决定,这里只补深红加粗与描边细节。
|
||||
前缀 .rv-cell 提高特异性,压过 Element 的 .el-tag--dark.el-tag--danger 变量定义 */
|
||||
.rv-cell .rv-chip {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--overdue,
|
||||
.rv-cell .rv-chip--missed {
|
||||
--el-tag-bg-color: #dc2626;
|
||||
--el-tag-border-color: #dc2626;
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--severe,
|
||||
.rv-cell .rv-chip--overdue.rv-chip--severe,
|
||||
.rv-cell .rv-chip--missed.rv-chip--severe {
|
||||
font-weight: 700;
|
||||
--el-tag-bg-color: #b91c1c;
|
||||
--el-tag-border-color: #b91c1c;
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--warning {
|
||||
--el-tag-text-color: #ea580c;
|
||||
--el-tag-bg-color: rgba(234, 88, 12, 0.1);
|
||||
--el-tag-border-color: rgba(234, 88, 12, 0.3);
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--no_days {
|
||||
--el-tag-text-color: #ea580c;
|
||||
--el-tag-border-color: #ea580c;
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--visited {
|
||||
--el-tag-text-color: #16a34a;
|
||||
--el-tag-bg-color: rgba(22, 163, 74, 0.1);
|
||||
--el-tag-border-color: rgba(22, 163, 74, 0.3);
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--booked {
|
||||
--el-tag-text-color: #4a5dff;
|
||||
--el-tag-bg-color: rgba(74, 93, 255, 0.1);
|
||||
--el-tag-border-color: rgba(74, 93, 255, 0.3);
|
||||
}
|
||||
|
||||
.rv-cell .rv-chip--contacted {
|
||||
--el-tag-text-color: #4a5dff;
|
||||
--el-tag-border-color: #4a5dff;
|
||||
}
|
||||
|
||||
.rv-line--follow {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.rv-none {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.rv-tooltip {
|
||||
line-height: 1.6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* H5 紧凑模式 */
|
||||
.rv-cell--compact {
|
||||
gap: 2px;
|
||||
|
||||
.rv-line--head,
|
||||
.rv-line--due,
|
||||
.rv-prescribed {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rv-line--order,
|
||||
.rv-line--follow,
|
||||
.rv-doctor {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.rv-none {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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 @@
|
||||
待预约
|
||||
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
|
||||
</span>
|
||||
<!-- 复诊到期:到期未挂号 ∪ 过号未面诊(revisit_state=due),点击默认按药尽日升序 -->
|
||||
<span
|
||||
:class="['chip', 'chip-revisit-due', { active: formData.revisit_state === 'due' }]"
|
||||
@click="handleRevisitDueTabClick"
|
||||
>
|
||||
复诊到期
|
||||
<em v-if="revisitDueCount !== undefined">{{ revisitDueCount }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['chip', 'chip-completed-visit', { active: formData.completed_appointment === '1' }]"
|
||||
@click="handleCompletedVisitTabClick"
|
||||
@@ -147,6 +156,36 @@
|
||||
:value="String(item.value)"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="formData.revisit_state"
|
||||
placeholder="复诊状态"
|
||||
clearable
|
||||
size="small"
|
||||
class="revisit-state-select"
|
||||
@change="handleRevisitFilterChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in revisitStateFilterOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="formData.revisit_days_bucket"
|
||||
placeholder="开药天数档"
|
||||
clearable
|
||||
size="small"
|
||||
class="revisit-days-bucket-select"
|
||||
@change="doSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in revisitDaysBucketOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -278,22 +317,20 @@
|
||||
<span v-else class="status-unconfirmed">未确认</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="复诊" min-width="120">
|
||||
<!-- 复诊节点:吞掉原「复诊」「开方」两列;排序走后端 sort_revisit_due(药尽日,NULL 置后) -->
|
||||
<el-table-column
|
||||
label="复诊节点"
|
||||
prop="revisit_due_date"
|
||||
min-width="180"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.has_prescription" class="followup-cell">
|
||||
<div class="followup-time">{{ row.followup_time_text || '—' }}</div>
|
||||
<div class="followup-doctor">{{ row.followup_doctor_name || '—' }}</div>
|
||||
<el-tag
|
||||
v-if="row.followup_rx_voided"
|
||||
type="info"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="followup-void-tag"
|
||||
>
|
||||
处方已作废
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="followup-none">无</span>
|
||||
<revisit-node-cell
|
||||
:due="revisitDueOf(row)"
|
||||
:has-prescription="!!row.has_prescription"
|
||||
:followup-doctor-name="row.followup_doctor_name || ''"
|
||||
:prescribed-text="prescribedTextOf(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="助理" width="100" show-overflow-tooltip>
|
||||
@@ -301,12 +338,6 @@
|
||||
<span class="assistant-cell">{{ getAssistantName(row.assistant_id || row.assistant) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开方" width="72" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.has_prescription" class="status-prescribed">已开方</span>
|
||||
<span v-else class="status-unprescribed">未开方</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="未服务天数"
|
||||
prop="unserved_days"
|
||||
@@ -519,7 +550,6 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="继承">
|
||||
<el-checkbox v-model="assignForm.is_inherit">继承</el-checkbox>
|
||||
<span class="text-gray-400 text-xs ml-2">勾选后写入指派记录</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="assignMode === 'batch'" label="已选择">
|
||||
<div class="text-gray-500">{{ batchAssignIds.length }} 条诊单记录</div>
|
||||
@@ -737,7 +767,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="tcmDiagnosis">
|
||||
import { tcmDiagnosisLists, tcmDiagnosisDelete, tcmDiagnosisAssign, tcmDiagnosisGuahaoLogList, getAssistants, generateMiniProgramQrcode, generateOrderQrcode, getWechatChatRecords, addWechatChatRecord, deleteWechatChatRecord, getWechatExternalContact, fillIdCard } from '@/api/tcm'
|
||||
import { tcmDiagnosisLists, tcmDiagnosisDelete, tcmDiagnosisAssign, tcmDiagnosisGuahaoLogList, getAssistants, generateMiniProgramQrcode, generateOrderQrcode, getWechatChatRecords, addWechatChatRecord, deleteWechatChatRecord, getWechatExternalContact, fillIdCard, diagnosisReadonlyDetail } from '@/api/tcm'
|
||||
import { orderCreate } from '@/api/order'
|
||||
import { cancelAppointment } from '@/api/doctor'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
@@ -752,6 +782,15 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import DaterangePicker from '@/components/daterange-picker/index.vue'
|
||||
import RevisitNodeCell from './components/RevisitNodeCell.vue'
|
||||
import { formatGender } from '@/utils/diag-display'
|
||||
import {
|
||||
normalizeRevisitDue,
|
||||
REVISIT_DAYS_BUCKET_OPTIONS,
|
||||
REVISIT_STATE_FILTER_OPTIONS,
|
||||
revisitRowClassName,
|
||||
type RevisitDue
|
||||
} from '@/utils/revisitDue'
|
||||
|
||||
const EditPopup = defineAsyncComponent(() => import('./edit.vue'))
|
||||
const DetailPopup = defineAsyncComponent(() => import('./detail.vue'))
|
||||
@@ -798,6 +837,12 @@ const formData = reactive({
|
||||
latest_assign_end_date: '' as string,
|
||||
/** 未服务天数排序:desc=天数多到少 asc=少到多 */
|
||||
sort_unserved_days: '' as '' | 'asc' | 'desc',
|
||||
/** 复诊状态筛选:due(到期未挂号∪过号)|warning|booked|visited|contacted|no_days|pending,空=不限 */
|
||||
revisit_state: '' as string,
|
||||
/** 开药天数档筛选:14|21|28|30|other,空=不限 */
|
||||
revisit_days_bucket: '' as string,
|
||||
/** 复诊节点(药尽日)排序:asc=最早到期在前(NULL 置后) desc 反之;与 sort_unserved_days 互斥 */
|
||||
sort_revisit_due: '' as '' | 'asc' | 'desc',
|
||||
diagnosis_confirmed: '' as '' | '0' | '1',
|
||||
appointment_date: '' as string,
|
||||
has_appointment: '' as '' | '0' | '1',
|
||||
@@ -898,11 +943,13 @@ function buildSharedDiagnosisFilterPayload(): Record<string, unknown> {
|
||||
latest_appointment_end_date: formData.latest_appointment_end_date,
|
||||
latest_appointment_channel_source: formData.latest_appointment_channel_source,
|
||||
latest_assign_start_date: formData.latest_assign_start_date,
|
||||
latest_assign_end_date: formData.latest_assign_end_date
|
||||
latest_assign_end_date: formData.latest_assign_end_date,
|
||||
revisit_state: formData.revisit_state,
|
||||
revisit_days_bucket: formData.revisit_days_bucket
|
||||
}
|
||||
}
|
||||
|
||||
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件 */
|
||||
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件(revisit_state 视为 Tab 条件,角标间互不影响) */
|
||||
function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
page_no: 1,
|
||||
@@ -915,6 +962,8 @@ function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}):
|
||||
pending_assign_order_month: '',
|
||||
pending_assign_keyword: '',
|
||||
sort_unserved_days: '',
|
||||
revisit_state: '',
|
||||
sort_revisit_due: '',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
@@ -960,6 +1009,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
|
||||
formData.latest_assign_start_date = ''
|
||||
formData.latest_assign_end_date = ''
|
||||
formData.sort_unserved_days = ''
|
||||
formData.revisit_state = ''
|
||||
formData.revisit_days_bucket = ''
|
||||
formData.sort_revisit_due = ''
|
||||
formData.pending_assign_order_month = ''
|
||||
activeTab.value = 'all'
|
||||
if (kw1 !== '') {
|
||||
@@ -996,6 +1048,7 @@ const handleDateTabClick = async (value: string) => {
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
clearRevisitTabFilters()
|
||||
clearLatestAppointmentFilters()
|
||||
formData.appointment_date = value
|
||||
pager.page = 1
|
||||
@@ -1012,6 +1065,7 @@ const handlePendingBookingTabClick = async () => {
|
||||
formData.completed_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = '0'
|
||||
clearRevisitTabFilters()
|
||||
clearLatestAppointmentFilters()
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
@@ -1027,6 +1081,7 @@ const handleCompletedVisitTabClick = async () => {
|
||||
formData.completed_appointment = '1'
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
clearRevisitTabFilters()
|
||||
clearLatestAppointmentFilters()
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
@@ -1039,6 +1094,7 @@ const handlePendingAssignTabClick = async () => {
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
clearRevisitTabFilters()
|
||||
clearLatestAppointmentFilters()
|
||||
if (!formData.pending_assign_order_month) {
|
||||
formData.pending_assign_order_month = dayjs().format('YYYY-MM')
|
||||
@@ -1057,10 +1113,66 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
// 获取各日期挂号数量 + 全部 + 待预约 + 已完成 + 待分配医助数量
|
||||
/** 复诊状态 / 开药天数档 下拉选项(文案与请求值映射在 utils/revisitDue.ts) */
|
||||
const revisitStateFilterOptions = REVISIT_STATE_FILTER_OPTIONS
|
||||
const revisitDaysBucketOptions = REVISIT_DAYS_BUCKET_OPTIONS
|
||||
|
||||
/** 顶部「复诊到期」角标:revisit_state=due 的 count */
|
||||
const revisitDueCount = ref<number | undefined>(undefined)
|
||||
|
||||
/** 切换其它顶部 Tab 时清掉复诊 chip 状态与其排序(revisit_days_bucket 属「更多筛选」,保留) */
|
||||
const clearRevisitTabFilters = () => {
|
||||
formData.revisit_state = ''
|
||||
formData.sort_revisit_due = ''
|
||||
}
|
||||
|
||||
/** 顶部「复诊到期」chip:到期未挂号 ∪ 过号未面诊,默认按药尽日升序(超期最多在前) */
|
||||
const handleRevisitDueTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_assign_order_month = ''
|
||||
formData.pending_assign_keyword = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = ''
|
||||
clearLatestAppointmentFilters()
|
||||
formData.revisit_state = 'due'
|
||||
formData.sort_revisit_due = 'asc'
|
||||
formData.sort_unserved_days = ''
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
/** 「更多筛选 → 复诊状态」下拉:与顶部日期 Tab 互斥(同 chip 语义),选中后不再默认当天挂号 */
|
||||
const handleRevisitFilterChange = () => {
|
||||
if (formData.revisit_state !== '') {
|
||||
formData.appointment_date = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
const noSortYet = formData.sort_revisit_due === '' && formData.sort_unserved_days === ''
|
||||
if (formData.revisit_state === 'due' && noSortYet) {
|
||||
formData.sort_revisit_due = 'asc'
|
||||
}
|
||||
} else {
|
||||
formData.sort_revisit_due = ''
|
||||
}
|
||||
doSearch()
|
||||
}
|
||||
|
||||
/** 列表行 revisit_due → 规整对象(旧接口无字段时为 null,单元格显示「—」) */
|
||||
const revisitDueOf = (row: any): RevisitDue | null => normalizeRevisitDue(row?.revisit_due)
|
||||
|
||||
/** 「复诊节点」行 1 的开方文案:已开方 / 已开方(处方已作废) / 未开方 */
|
||||
const prescribedTextOf = (row: any): string => {
|
||||
if (!row?.has_prescription) return '未开方'
|
||||
return row.followup_rx_voided ? '已开方(处方已作废)' : '已开方'
|
||||
}
|
||||
|
||||
// 获取各日期挂号数量 + 全部 + 待预约 + 已完成 + 待分配医助数量 + 复诊到期数量
|
||||
const fetchDateCounts = async () => {
|
||||
try {
|
||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending, revisitDue] = await Promise.all([
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
|
||||
),
|
||||
@@ -1081,7 +1193,10 @@ const fetchDateCounts = async () => {
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
|
||||
),
|
||||
tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
|
||||
tcmDiagnosisLists(buildPendingAssignCountPayload() as any),
|
||||
tcmDiagnosisLists(
|
||||
buildDateCountRequestPayload({ revisit_state: 'due', has_appointment: '' }) as any
|
||||
)
|
||||
])
|
||||
dateCounts.value = {
|
||||
[yesterdayStr.value]: yesterday?.count ?? 0,
|
||||
@@ -1094,6 +1209,7 @@ const fetchDateCounts = async () => {
|
||||
pendingBookingCount.value = noApt?.count ?? 0
|
||||
completedVisitCount.value = doneVisit?.count ?? 0
|
||||
pendingAssignCount.value = pending?.count ?? 0
|
||||
revisitDueCount.value = revisitDue?.count ?? 0
|
||||
} catch (e) {
|
||||
console.error('获取日期数量失败', e)
|
||||
}
|
||||
@@ -1252,12 +1368,11 @@ const handleTableSortChange = ({
|
||||
prop: string
|
||||
order: 'ascending' | 'descending' | null
|
||||
}) => {
|
||||
if (prop === 'unserved_days') {
|
||||
formData.sort_unserved_days =
|
||||
order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
|
||||
} else {
|
||||
formData.sort_unserved_days = ''
|
||||
}
|
||||
const dir: '' | 'asc' | 'desc' =
|
||||
order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
|
||||
// 按列 prop 分派;两种排序后端互斥,切换列时清掉另一列
|
||||
formData.sort_unserved_days = prop === 'unserved_days' ? dir : ''
|
||||
formData.sort_revisit_due = prop === 'revisit_due_date' ? dir : ''
|
||||
pager.page = 1
|
||||
getLists()
|
||||
}
|
||||
@@ -1311,6 +1426,9 @@ const handleReset = () => {
|
||||
formData.latest_assign_start_date = ''
|
||||
formData.latest_assign_end_date = ''
|
||||
formData.sort_unserved_days = ''
|
||||
formData.revisit_state = ''
|
||||
formData.revisit_days_bucket = ''
|
||||
formData.sort_revisit_due = ''
|
||||
formData.diagnosis_confirmed = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = ''
|
||||
@@ -1343,8 +1461,10 @@ const unservedDaysClass = (n: unknown) => {
|
||||
return 'unserved-green'
|
||||
}
|
||||
|
||||
// 行高亮:未确认诊单优先,其次未挂号
|
||||
// 行高亮优先级(方案 D3-c 默认 overdue_first):复诊超期/过号 > 未确认 > 未挂号
|
||||
const getRowClassName = ({ row }: { row: any }) => {
|
||||
const revisitCls = revisitRowClassName(revisitDueOf(row))
|
||||
if (revisitCls) return revisitCls
|
||||
if (!isDiagnosisConfirmed(row)) return 'row-unconfirmed'
|
||||
if (!row.has_appointment) return 'row-no-appointment'
|
||||
return ''
|
||||
@@ -1356,6 +1476,8 @@ const prescriptionRef = ref()
|
||||
const appointmentRef = ref()
|
||||
/** 从路由带 id 进入时,诊单弹窗为异步组件,需在 ref 就绪后再 open */
|
||||
const pendingRouteDiagnosisId = ref<number | null>(null)
|
||||
/** 路由 ?id=&action=appointment 深链(复诊到期弹窗「去挂号」):挂号弹窗同为异步组件,先暂存行再 open */
|
||||
const pendingRouteAppointmentRow = ref<Record<string, any> | null>(null)
|
||||
const userStore = useUserStore()
|
||||
|
||||
watch(
|
||||
@@ -1370,6 +1492,53 @@ watch(
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
watch(
|
||||
[appointmentRef, pendingRouteAppointmentRow],
|
||||
() => {
|
||||
const row = pendingRouteAppointmentRow.value
|
||||
const opener = appointmentRef.value as { open?: (row: any) => void } | undefined
|
||||
if (!row || !opener?.open) return
|
||||
pendingRouteAppointmentRow.value = null
|
||||
handleAppointment(row)
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
/**
|
||||
* 深链打开挂号弹窗:优先用当前列表里的行(字段最全);不在本页时回退只读详情接口拼最小行
|
||||
* (appointment.vue open() 只读 patient_id / patient_name / gender_desc / age)。
|
||||
*/
|
||||
const openAppointmentFromRoute = async (diagnosisId: number) => {
|
||||
// 深链打开挂号弹窗前先校验挂号权限(与行内「预约」按钮 v-perms 同口径)
|
||||
if (!hasPermission(['tcm.diagnosis/guahao'])) {
|
||||
feedback.msgWarning('无挂号权限')
|
||||
return
|
||||
}
|
||||
const inList = (pager.lists as any[]).find((r) => Number(r?.id) === diagnosisId)
|
||||
if (inList) {
|
||||
pendingRouteAppointmentRow.value = inList
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res: any = await diagnosisReadonlyDetail({ id: diagnosisId })
|
||||
const diag = res?.diagnosis
|
||||
if (!diag || !diag.patient_id) {
|
||||
feedback.msgWarning('未找到该诊单或患者信息不完整,无法打开挂号')
|
||||
return
|
||||
}
|
||||
pendingRouteAppointmentRow.value = {
|
||||
...diag,
|
||||
id: diagnosisId,
|
||||
patient_id: diag.patient_id,
|
||||
patient_name: diag.patient_name || '',
|
||||
gender_desc: formatGender(diag.gender),
|
||||
age: diag.age ?? 0
|
||||
}
|
||||
} catch (e: any) {
|
||||
feedback.msgWarning(e?.msg || e?.message || '加载诊单失败,无法打开挂号')
|
||||
}
|
||||
}
|
||||
|
||||
const watchCallVisible = ref(false)
|
||||
const watchCallDiagnosisId = ref(0)
|
||||
|
||||
@@ -1614,13 +1783,13 @@ const handleConfirmAssign = async () => {
|
||||
feedback.msgWarning('请选择医助')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
assignLoading.value = true
|
||||
try {
|
||||
if (assignMode.value === 'single') {
|
||||
// 单独指派
|
||||
await tcmDiagnosisAssign({
|
||||
id: currentRow.value.id,
|
||||
await tcmDiagnosisAssign({
|
||||
id: currentRow.value.id,
|
||||
assistant_id: assignForm.value.assistant_id,
|
||||
is_inherit: assignForm.value.is_inherit ? 1 : 0
|
||||
})
|
||||
@@ -2220,22 +2389,57 @@ const goReadonly = (row: any) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认展示当天挂号,突出重点
|
||||
formData.appointment_date = todayStr.value
|
||||
// 字典与列表并行,避免 await 字典阻塞首屏列表(LCP)
|
||||
getDictOptions()
|
||||
getLists()
|
||||
scheduleDeferredDateCounts()
|
||||
if (String(route.query.revisit_state ?? '') === 'due') {
|
||||
// 复诊到期弹窗「查看全部」深链:直接激活「复诊到期」chip(内部会 getLists + fetchDateCounts)
|
||||
void handleRevisitDueTabClick()
|
||||
} else {
|
||||
// 默认展示当天挂号,突出重点
|
||||
formData.appointment_date = todayStr.value
|
||||
getLists()
|
||||
scheduleDeferredDateCounts()
|
||||
}
|
||||
|
||||
silentListPollTimer = setInterval(silentRefreshListsAndDateCounts, SILENT_LIST_POLL_MS)
|
||||
|
||||
const raw = route.query.id
|
||||
if (raw != null && raw !== '') {
|
||||
const num = Number(raw)
|
||||
if (!Number.isNaN(num)) pendingRouteDiagnosisId.value = num
|
||||
if (!Number.isNaN(num) && num > 0) {
|
||||
if (String(route.query.action ?? '') === 'appointment') {
|
||||
// 复诊到期弹窗「去挂号」深链:等首屏列表回来后再找行,找不到走详情接口
|
||||
void getListsPromiseForRoute().then(() => openAppointmentFromRoute(num))
|
||||
} else {
|
||||
pendingRouteDiagnosisId.value = num
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** onMounted 里 getLists() 已发起;等待首屏列表 loading 结束(最多 8s)再找行,失败/超时不阻断 */
|
||||
const getListsPromiseForRoute = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (!pager.loading) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
let settled = false
|
||||
const finish = () => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
stop()
|
||||
resolve()
|
||||
}
|
||||
const stop = watch(
|
||||
() => pager.loading,
|
||||
(loading) => {
|
||||
if (!loading) finish()
|
||||
}
|
||||
)
|
||||
setTimeout(finish, 8000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (silentListPollTimer != null) {
|
||||
clearInterval(silentListPollTimer)
|
||||
@@ -2313,6 +2517,21 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.chip-revisit-due {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-danger-light-8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.chip-completed-visit {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-success);
|
||||
@@ -2451,6 +2670,14 @@ onUnmounted(() => {
|
||||
.latest-appointment-channel {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.revisit-state-select {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.revisit-days-bucket-select {
|
||||
width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-card {
|
||||
@@ -2641,17 +2868,6 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-prescribed {
|
||||
font-size: 13px;
|
||||
color: #4A5DFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-unprescribed {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
/* 未服务天数(T3):null=灰;>=7=红;3-6=橙;<=2=绿 */
|
||||
.unserved-days {
|
||||
display: inline-block;
|
||||
@@ -2696,33 +2912,6 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.followup-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
line-height: 1.35;
|
||||
|
||||
.followup-time {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.followup-doctor {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.followup-void-tag {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.followup-none {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.assistant-cell {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
@@ -2782,6 +2971,22 @@ onUnmounted(() => {
|
||||
:deep(.el-table__row.row-no-appointment:hover > td) {
|
||||
background: linear-gradient(90deg, rgba(var(--el-color-info-rgb), 0.08) 0%, transparent 100%) !important;
|
||||
}
|
||||
/* 复诊到期未挂号 / 过号未面诊:红色左边条(同 row-unconfirmed 结构,warning → danger) */
|
||||
:deep(.el-table__row.row-revisit-overdue) {
|
||||
border-left: 3px solid var(--el-color-danger);
|
||||
background: linear-gradient(90deg, rgba(var(--el-color-danger-rgb), 0.06) 0%, transparent 100%) !important;
|
||||
}
|
||||
:deep(.el-table__row.row-revisit-overdue:hover > td) {
|
||||
background: linear-gradient(90deg, rgba(var(--el-color-danger-rgb), 0.1) 0%, transparent 100%) !important;
|
||||
}
|
||||
/* 超期 ≥ severe_overdue_days:底色加深到 10%,悬停 14% */
|
||||
:deep(.el-table__row.row-revisit-overdue.row-revisit-overdue-deep) {
|
||||
border-left-color: #b91c1c;
|
||||
background: linear-gradient(90deg, rgba(var(--el-color-danger-rgb), 0.1) 0%, transparent 100%) !important;
|
||||
}
|
||||
:deep(.el-table__row.row-revisit-overdue.row-revisit-overdue-deep:hover > td) {
|
||||
background: linear-gradient(90deg, rgba(var(--el-color-danger-rgb), 0.14) 0%, transparent 100%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.record-list {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
formData.pending_assign !== '1' &&
|
||||
formData.pending_booking !== '1' &&
|
||||
formData.completed_appointment !== '1' &&
|
||||
formData.revisit_state === '' &&
|
||||
formData.appointment_date === tab.value
|
||||
}
|
||||
]"
|
||||
@@ -40,6 +41,13 @@
|
||||
待预约
|
||||
<em v-if="pendingBookingCount !== undefined">{{ pendingBookingCount }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['dh5-chip', 'dh5-chip--revisit', { active: formData.revisit_state === 'due' }]"
|
||||
@click="handleRevisitDueTabClick"
|
||||
>
|
||||
复诊到期
|
||||
<em v-if="revisitDueCount !== undefined">{{ revisitDueCount }}</em>
|
||||
</span>
|
||||
<span
|
||||
:class="['dh5-chip', 'dh5-chip--done', { active: formData.completed_appointment === '1' }]"
|
||||
@click="handleCompletedVisitTabClick"
|
||||
@@ -170,13 +178,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 复诊块(仅已开方时显示) -->
|
||||
<div v-if="row.has_prescription" class="dh5-row dh5-row--followup">
|
||||
<!-- 复诊节点块(已开方或有复诊节点数据时显示;卡片头已有「已开方」标签,此处隐藏开方文案) -->
|
||||
<div v-if="row.has_prescription || hasRevisitDue(revisitDueOf(row))" class="dh5-row dh5-row--followup">
|
||||
<span class="dh5-row-key">复诊</span>
|
||||
<div class="dh5-row-val">
|
||||
<span>{{ row.followup_time_text || '—' }}</span>
|
||||
<span class="dh5-meta-doctor">{{ row.followup_doctor_name || '—' }}</span>
|
||||
<span v-if="row.followup_rx_voided" class="dh5-tag dh5-tag--gray">处方作废</span>
|
||||
<div class="dh5-row-val dh5-row-val--revisit">
|
||||
<revisit-node-cell
|
||||
compact
|
||||
:due="revisitDueOf(row)"
|
||||
:has-prescription="!!row.has_prescription"
|
||||
:followup-doctor-name="row.followup_doctor_name || ''"
|
||||
:prescribed-text="row.followup_rx_voided ? '处方作废' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -625,6 +637,13 @@ import {
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { defineAsyncComponent, computed, reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import RevisitNodeCell from './components/RevisitNodeCell.vue'
|
||||
import {
|
||||
hasRevisitDue,
|
||||
isRevisitDueHighlight,
|
||||
normalizeRevisitDue,
|
||||
type RevisitDue
|
||||
} from '@/utils/revisitDue'
|
||||
|
||||
const EditPopup = defineAsyncComponent(() => import('./edit.vue'))
|
||||
const DetailPopup = defineAsyncComponent(() => import('./detail.vue'))
|
||||
@@ -685,7 +704,9 @@ const formData = reactive({
|
||||
has_appointment: '' as '' | '0' | '1',
|
||||
pending_assign: '' as '' | '1',
|
||||
pending_booking: '' as '' | '1',
|
||||
completed_appointment: '' as '' | '1'
|
||||
completed_appointment: '' as '' | '1',
|
||||
/** 顶部「复诊到期」chip:due = 到期未挂号 ∪ 过号未面诊(后端 revisit_state) */
|
||||
revisit_state: '' as '' | 'due'
|
||||
})
|
||||
|
||||
const showMoreFilter = ref(false)
|
||||
@@ -740,6 +761,7 @@ const handleReset = () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.revisit_state = ''
|
||||
pager.page = 1
|
||||
getLists()
|
||||
fetchDateCounts()
|
||||
@@ -767,6 +789,7 @@ const handleDateTabClick = async (value: string) => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.revisit_state = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = value
|
||||
pager.page = 1
|
||||
@@ -778,6 +801,7 @@ const handlePendingBookingTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = '1'
|
||||
formData.completed_appointment = ''
|
||||
formData.revisit_state = ''
|
||||
formData.appointment_date = ''
|
||||
formData.has_appointment = '0'
|
||||
pager.page = 1
|
||||
@@ -789,6 +813,7 @@ const handleCompletedVisitTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = '1'
|
||||
formData.revisit_state = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
pager.page = 1
|
||||
@@ -800,6 +825,7 @@ const handlePendingAssignTabClick = async () => {
|
||||
formData.pending_assign = '1'
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.revisit_state = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
pager.page = 1
|
||||
@@ -807,9 +833,27 @@ const handlePendingAssignTabClick = async () => {
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
/** 顶部「复诊到期」chip:到期未挂号 ∪ 过号未面诊;后端按 revisit_state=due 过滤,排序默认药尽日升序 */
|
||||
const handleRevisitDueTabClick = async () => {
|
||||
formData.pending_assign = ''
|
||||
formData.pending_booking = ''
|
||||
formData.completed_appointment = ''
|
||||
formData.has_appointment = ''
|
||||
formData.appointment_date = ''
|
||||
formData.revisit_state = 'due'
|
||||
pager.page = 1
|
||||
await getLists()
|
||||
fetchDateCounts()
|
||||
}
|
||||
|
||||
const revisitDueCount = ref<number | undefined>(undefined)
|
||||
|
||||
/** 列表行 revisit_due → 规整对象(旧接口无字段时为 null,不渲染复诊块) */
|
||||
const revisitDueOf = (row: any): RevisitDue | null => normalizeRevisitDue(row?.revisit_due)
|
||||
|
||||
const fetchDateCounts = async () => {
|
||||
try {
|
||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
|
||||
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending, revisitDue] = await Promise.all([
|
||||
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
|
||||
@@ -818,7 +862,8 @@ 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({ pending_assign: 1, page_no: 1, page_size: 1 })
|
||||
tcmDiagnosisLists({ pending_assign: 1, page_no: 1, page_size: 1 }),
|
||||
tcmDiagnosisLists({ revisit_state: 'due', page_no: 1, page_size: 1 })
|
||||
])
|
||||
dateCounts.value = {
|
||||
[yesterdayStr.value]: yesterday?.count ?? 0,
|
||||
@@ -831,6 +876,7 @@ const fetchDateCounts = async () => {
|
||||
pendingBookingCount.value = noApt?.count ?? 0
|
||||
completedVisitCount.value = doneVisit?.count ?? 0
|
||||
pendingAssignCount.value = pending?.count ?? 0
|
||||
revisitDueCount.value = revisitDue?.count ?? 0
|
||||
} catch (e) {
|
||||
console.error('获取日期数量失败', e)
|
||||
}
|
||||
@@ -898,7 +944,9 @@ const isDiagnosisConfirmed = (row: any) => {
|
||||
return records.length > 0 && records.some((r: any) => r.is_confirmed == 1)
|
||||
}
|
||||
|
||||
// 卡片左边条优先级(同 PC 行高亮):复诊超期/过号 > 未确认 > 未挂号
|
||||
const getCardClass = (row: any) => {
|
||||
if (isRevisitDueHighlight(revisitDueOf(row))) return 'dh5-card--overdue'
|
||||
if (!isDiagnosisConfirmed(row)) return 'dh5-card--unconfirmed'
|
||||
if (!row.has_appointment) return 'dh5-card--noapt'
|
||||
return ''
|
||||
@@ -1582,6 +1630,8 @@ $dh5-card-bg: #ffffff;
|
||||
.dh5-chip--booking.active { background: $dh5-warn; }
|
||||
.dh5-chip--done.active { background: $dh5-success; }
|
||||
.dh5-chip--pending.active { background: $dh5-danger; }
|
||||
.dh5-chip--revisit { color: #b91c1c; background: rgba(239, 68, 68, 0.10); }
|
||||
.dh5-chip--revisit.active { color: #fff; background: $dh5-danger; }
|
||||
|
||||
.dh5-quick {
|
||||
display: flex;
|
||||
@@ -1675,6 +1725,12 @@ $dh5-card-bg: #ffffff;
|
||||
|
||||
.dh5-card--unconfirmed::before { background: $dh5-danger; }
|
||||
.dh5-card--noapt::before { background: $dh5-warn; }
|
||||
/* 复诊到期未挂号 / 过号未面诊:左边条同 danger,外框与底色轻微染红以区分「未确认」 */
|
||||
.dh5-card--overdue::before { background: $dh5-danger; }
|
||||
.dh5-card--overdue {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, $dh5-card-bg 60%);
|
||||
}
|
||||
|
||||
.dh5-card-head {
|
||||
display: flex;
|
||||
@@ -1800,6 +1856,11 @@ $dh5-card-bg: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 复诊节点块:单元格组件自带多行布局,取消外层 flex 换行间距 */
|
||||
.dh5-row-val--revisit {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 挂号块 - 状态色 */
|
||||
.dh5-row--apt {
|
||||
background: #fafbfc;
|
||||
|
||||
@@ -25,6 +25,17 @@
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="unserved-badge unserved-muted">从未打卡</span>
|
||||
<!-- 复诊节点徽标:第 N 诊 · 药尽 MM-DD · 超期 N 天(接口无 revisit_due 字段时不渲染) -->
|
||||
<el-tooltip
|
||||
v-if="revisitBadgeLabel"
|
||||
placement="bottom"
|
||||
:disabled="!revisitTooltipText"
|
||||
:content="revisitTooltipText"
|
||||
>
|
||||
<span :class="['unserved-badge', 'revisit-badge', revisitBadgeColorClass]">
|
||||
{{ revisitBadgeLabel }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,6 +167,12 @@ import { diagnosisReadonlyDetail } from '@/api/tcm'
|
||||
import { getDoctorNotes } from '@/api/patient'
|
||||
import { formatGender } from '@/utils/diag-display'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
import {
|
||||
normalizeRevisitDue,
|
||||
revisitBadgeText,
|
||||
revisitStateColorClass,
|
||||
revisitTooltipLines
|
||||
} from '@/utils/revisitDue'
|
||||
import PatientInfoCard from './components/PatientInfoCard.vue'
|
||||
import PatientCaseCard from './components/PatientCaseCard.vue'
|
||||
import DailyMatrix from './components/DailyMatrix.vue'
|
||||
@@ -193,6 +210,17 @@ const unservedDays = computed<number | null>(() => {
|
||||
})
|
||||
const lastBloodAt = computed<string>(() => detail.value?.last_blood_record_at || '')
|
||||
|
||||
/** 复诊节点:readonlyDetail 顶层 revisit_due(兼容挂在 diagnosis 下);字段不存在 → null → 不渲染徽标 */
|
||||
const revisitDue = computed(() =>
|
||||
normalizeRevisitDue(detail.value?.revisit_due ?? detail.value?.diagnosis?.revisit_due)
|
||||
)
|
||||
const revisitBadgeLabel = computed(() => revisitBadgeText(revisitDue.value))
|
||||
const revisitBadgeColorClass = computed(() => {
|
||||
const due = revisitDue.value
|
||||
return due ? revisitStateColorClass(due.state) : 'unserved-muted'
|
||||
})
|
||||
const revisitTooltipText = computed(() => revisitTooltipLines(revisitDue.value).join(';'))
|
||||
|
||||
/**
|
||||
* 「未服务天数」着色规则(与 T3 列表一致):
|
||||
* null/undefined → 灰;>=7 → 红;3-6 → 橙;<=2 → 绿
|
||||
@@ -352,6 +380,13 @@ onMounted(fetchDetail)
|
||||
border-color: #fecaca;
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
}
|
||||
|
||||
/* 复诊节点徽标专用:已约 / 已联系 */
|
||||
&.unserved-blue {
|
||||
color: #4a5dff;
|
||||
border-color: #c7d2fe;
|
||||
background: rgba(74, 93, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
Reference in New Issue
Block a user