This commit is contained in:
Your Name
2026-07-07 09:24:40 +08:00
parent 55e3b5d9e0
commit 99eca8b2e5
2 changed files with 218 additions and 44 deletions
+122 -20
View File
@@ -121,6 +121,16 @@
end-placeholder="最近挂号结束" end-placeholder="最近挂号结束"
@change="handleLatestAppointmentFilterChange" @change="handleLatestAppointmentFilterChange"
/> />
<daterange-picker
class="latest-assign-range"
v-model:startTime="formData.latest_assign_start_date"
v-model:endTime="formData.latest_assign_end_date"
picker-type="daterange"
value-format="YYYY-MM-DD"
start-placeholder="最近指派开始"
end-placeholder="最近指派结束"
@change="handleLatestAssignFilterChange"
/>
<el-select <el-select
v-model="formData.latest_appointment_channel_source" v-model="formData.latest_appointment_channel_source"
placeholder="最近挂号渠道" placeholder="最近挂号渠道"
@@ -173,6 +183,7 @@
v-loading="pager.loading" v-loading="pager.loading"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
@row-dblclick="goReadonly" @row-dblclick="goReadonly"
@sort-change="handleTableSortChange"
:row-class-name="getRowClassName" :row-class-name="getRowClassName"
class="diagnosis-table" class="diagnosis-table"
stripe stripe
@@ -283,7 +294,14 @@
<span v-else class="status-unprescribed">未开方</span> <span v-else class="status-unprescribed">未开方</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="未服务天数" width="110" align="center"> <el-table-column
label="未服务天数"
prop="unserved_days"
width="110"
align="center"
sortable="custom"
:sort-orders="['descending', 'ascending']"
>
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip <el-tooltip
v-if="row.last_blood_record_at" v-if="row.last_blood_record_at"
@@ -762,6 +780,10 @@ const formData = reactive({
latest_appointment_start_date: '' as string, latest_appointment_start_date: '' as string,
latest_appointment_end_date: '' as string, latest_appointment_end_date: '' as string,
latest_appointment_channel_source: '' as string, latest_appointment_channel_source: '' as string,
latest_assign_start_date: '' as string,
latest_assign_end_date: '' as string,
/** 未服务天数排序:desc=天数多到少 asc=少到多 */
sort_unserved_days: '' as '' | 'asc' | 'desc',
diagnosis_confirmed: '' as '' | '0' | '1', diagnosis_confirmed: '' as '' | '0' | '1',
appointment_date: '' as string, appointment_date: '' as string,
has_appointment: '' as '' | '0' | '1', has_appointment: '' as '' | '0' | '1',
@@ -849,22 +871,50 @@ function resolvePendingAssignOrderMonthForRequest(): string {
return dayjs().format('YYYY-MM') return dayjs().format('YYYY-MM')
} }
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」 */ /** 除顶部 Tab 专属条件外,与主列表共用的「更多筛选」参数(角标 count 需同步 */
function buildPendingAssignCountPayload(): Record<string, unknown> { function buildSharedDiagnosisFilterPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload({ return {
...formData, keyword: formData.keyword,
diagnosis_type: formData.diagnosis_type,
syndrome_type: formData.syndrome_type,
assistant_id: formData.assistant_id,
diagnosis_confirmed: formData.diagnosis_confirmed,
has_appointment: formData.has_appointment,
latest_appointment_start_date: formData.latest_appointment_start_date,
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
}
}
/** 顶部 Tab 角标 count 请求:带上共用筛选,再叠加各 Tab 专属条件 */
function buildDateCountRequestPayload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
page_no: 1, page_no: 1,
page_size: 1, page_size: 1,
pending_assign: 1, ...buildSharedDiagnosisFilterPayload(),
appointment_date: '', appointment_date: '',
has_appointment: '',
pending_booking: '', pending_booking: '',
completed_appointment: '', completed_appointment: '',
latest_appointment_start_date: '', pending_assign: '',
latest_appointment_end_date: '', pending_assign_order_month: '',
latest_appointment_channel_source: '', pending_assign_keyword: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest() sort_unserved_days: '',
} as Record<string, unknown>) as Record<string, unknown> ...overrides
}
}
/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
function buildPendingAssignCountPayload(): Record<string, unknown> {
return buildTcmDiagnosisListRequestPayload(
buildDateCountRequestPayload({
pending_assign: 1,
has_appointment: '',
pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
pending_assign_keyword: formData.pending_assign_keyword
}) as Record<string, unknown>
) as Record<string, unknown>
} }
const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) => const fetchTcmDiagnosisListsForPaging = (req: Record<string, unknown>) =>
@@ -893,6 +943,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.pending_assign_order_month = '' formData.pending_assign_order_month = ''
activeTab.value = 'all' activeTab.value = 'all'
if (kw1 !== '') { if (kw1 !== '') {
@@ -994,14 +1047,26 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
const fetchDateCounts = async () => { const fetchDateCounts = async () => {
try { try {
const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([ const [yesterday, dayBefore, today, tomorrow, dayAfter, all, noApt, doneVisit, pending] = await Promise.all([
tcmDiagnosisLists({ appointment_date: yesterdayStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
tcmDiagnosisLists({ page_no: 1, page_size: 1 }), ),
tcmDiagnosisLists({ has_appointment: 0, page_no: 1, page_size: 1 }), tcmDiagnosisLists(
tcmDiagnosisLists({ completed_appointment: 1, page_no: 1, page_size: 1 }), buildDateCountRequestPayload({ appointment_date: todayStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: tomorrowStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(
buildDateCountRequestPayload({ appointment_date: dayAfterStr.value, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildDateCountRequestPayload() as any),
tcmDiagnosisLists(buildDateCountRequestPayload({ has_appointment: 0 }) as any),
tcmDiagnosisLists(
buildDateCountRequestPayload({ completed_appointment: 1, has_appointment: '' }) as any
),
tcmDiagnosisLists(buildPendingAssignCountPayload() as any) tcmDiagnosisLists(buildPendingAssignCountPayload() as any)
]) ])
dateCounts.value = { dateCounts.value = {
@@ -1135,6 +1200,11 @@ const clearLatestAppointmentFilters = () => {
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
} }
const clearLatestAssignFilters = () => {
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
}
const hasLatestAppointmentFilter = () => const hasLatestAppointmentFilter = () =>
!!( !!(
formData.latest_appointment_start_date || formData.latest_appointment_start_date ||
@@ -1142,6 +1212,9 @@ const hasLatestAppointmentFilter = () =>
formData.latest_appointment_channel_source formData.latest_appointment_channel_source
) )
const hasLatestAssignFilter = () =>
!!(formData.latest_assign_start_date || formData.latest_assign_end_date)
const handleLatestAppointmentFilterChange = () => { const handleLatestAppointmentFilterChange = () => {
if (hasLatestAppointmentFilter()) { if (hasLatestAppointmentFilter()) {
formData.appointment_date = '' formData.appointment_date = ''
@@ -1154,6 +1227,27 @@ const handleLatestAppointmentFilterChange = () => {
doSearch() doSearch()
} }
const handleLatestAssignFilterChange = () => {
doSearch()
}
const handleTableSortChange = ({
prop,
order
}: {
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 = ''
}
pager.page = 1
getLists()
}
const latestAppointmentChannelText = (row: any) => { const latestAppointmentChannelText = (row: any) => {
const desc = String(row?.latest_appointment_channel_source_desc || '').trim() const desc = String(row?.latest_appointment_channel_source_desc || '').trim()
const raw = String(row?.latest_appointment_channel_source || '').trim() const raw = String(row?.latest_appointment_channel_source || '').trim()
@@ -1200,6 +1294,9 @@ const handleReset = () => {
formData.latest_appointment_start_date = '' formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = '' formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = '' formData.latest_appointment_channel_source = ''
formData.latest_assign_start_date = ''
formData.latest_assign_end_date = ''
formData.sort_unserved_days = ''
formData.diagnosis_confirmed = '' formData.diagnosis_confirmed = ''
formData.appointment_date = '' formData.appointment_date = ''
formData.has_appointment = '' formData.has_appointment = ''
@@ -2298,6 +2395,11 @@ onUnmounted(() => {
max-width: 100%; max-width: 100%;
} }
.latest-assign-range {
width: 260px;
max-width: 100%;
}
.latest-appointment-channel { .latest-appointment-channel {
width: 170px; width: 170px;
} }
@@ -140,6 +140,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
$this->applyPendingAssignBusinessOrderMonthFilter($query); $this->applyPendingAssignBusinessOrderMonthFilter($query);
@@ -167,30 +168,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
} }
// 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序 $orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
// 若传了 appointment_date(当天/明天等筛选),只按「该日」的挂号排序与展示
$diagTbl = (new Diagnosis())->getTable();
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
// 获取最早的挂号状态(用于排序优先级)
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
// 获取最早的挂号时间(用于同状态内排序)
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
// 「已完成」Tab:按最近一条「已完成」(status=3) 挂号日期+时间降序…
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
} else {
$orderRaw = $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
$lists = $query $lists = $query
->with(['DiagnosisViewRecord']) ->with(['DiagnosisViewRecord'])
@@ -571,6 +549,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
} }
$this->applyLatestAppointmentFilters($query, $pendingWideSearch); $this->applyLatestAppointmentFilters($query, $pendingWideSearch);
$this->applyLatestAssignFilters($query, $pendingWideSearch);
// 仅已开方(待分配+关键词检索时不限制) // 仅已开方(待分配+关键词检索时不限制)
if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') { if (!$pendingWideSearch && isset($this->params['only_has_prescription']) && (string) $this->params['only_has_prescription'] === '1') {
@@ -644,6 +623,99 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions)); $query->whereExists("SELECT 1 FROM {$aptTbl} latest_apt WHERE " . implode(' AND ', $conditions));
} }
/**
* 最近一次成功指派过滤:按 create_time DESC, id DESC 取 to_assistant_id>0 的一条。
*
* @param mixed $query
*/
private function applyLatestAssignFilters($query, bool $pendingWideSearch): void
{
if ($pendingWideSearch) {
return;
}
$startDate = $this->normalizeYmd($this->params['latest_assign_start_date'] ?? '');
$endDate = $this->normalizeYmd($this->params['latest_assign_end_date'] ?? '');
if ($startDate === '' && $endDate === '') {
return;
}
$logTbl = Db::name('tcm_diagnosis_assign_log')->getTable();
$diagTbl = (new Diagnosis())->getTable();
$latestIdSql = $this->latestAssignLogIdSubSql($logTbl, $diagTbl);
$conditions = ["latest_lg.id = ({$latestIdSql})"];
if ($startDate !== '') {
$startTs = (int) strtotime($startDate . ' 00:00:00');
$conditions[] = "latest_lg.create_time >= {$startTs}";
}
if ($endDate !== '') {
$endTs = (int) strtotime($endDate . ' 23:59:59');
$conditions[] = "latest_lg.create_time <= {$endTs}";
}
$query->whereExists("SELECT 1 FROM {$logTbl} latest_lg WHERE " . implode(' AND ', $conditions));
}
private function latestAssignLogIdSubSql(string $logTbl, string $diagTbl): string
{
return "SELECT lg_latest.id FROM {$logTbl} lg_latest "
. "WHERE lg_latest.diagnosis_id = {$diagTbl}.id "
. 'AND lg_latest.to_assistant_id > 0 '
. 'ORDER BY lg_latest.create_time DESC, lg_latest.id DESC LIMIT 1';
}
/**
* 列表默认排序;支持 sort_unserved_days=asc|desc 按未服务天数排序。
*/
private function resolveListOrderRaw(bool $pendingWideSearch): string
{
$diagTbl = (new Diagnosis())->getTable();
$sortUnserved = strtolower(trim((string) ($this->params['sort_unserved_days'] ?? '')));
if (in_array($sortUnserved, ['asc', 'desc'], true)) {
$anchorExpr = $this->unservedAnchorExpr($diagTbl);
$nullLast = "CASE WHEN IFNULL({$anchorExpr}, 0) = 0 THEN 1 ELSE 0 END ASC";
if ($sortUnserved === 'desc') {
return "{$nullLast}, IFNULL({$anchorExpr}, 0) ASC, {$diagTbl}.id DESC";
}
return "{$nullLast}, IFNULL({$anchorExpr}, 0) DESC, {$diagTbl}.id DESC";
}
$aptTbl = (new Appointment())->getTable();
$minAptDateCond = '';
if (!$pendingWideSearch && !empty($this->params['appointment_date'])) {
$sortAptDate = addslashes((string) $this->params['appointment_date']);
$minAptDateCond = " AND apt.appointment_date = '{$sortAptDate}'";
}
$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ' ORDER BY CASE apt.status WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END, apt.appointment_date ASC, apt.appointment_time ASC LIMIT 1)';
$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status IN (1,3,4)' . $minAptDateCond . ')';
$isCompletedTab = !$pendingWideSearch && isset($this->params['completed_appointment']) && (string) $this->params['completed_appointment'] === '1';
if ($isCompletedTab) {
$maxCompletedAptExpr = '(SELECT MAX(CONCAT(apt.appointment_date, \' \', IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) FROM ' . $aptTbl . ' apt WHERE apt.patient_id = ' . $diagTbl . '.id AND apt.status = 3' . $minAptDateCond . ')';
return $diagTbl . '.assign_read_at IS NULL DESC, IFNULL(' . $maxCompletedAptExpr . ", '1970-01-01 00:00:00') DESC, {$diagTbl}.id DESC";
}
return $diagTbl . '.assign_read_at IS NULL DESC, CASE IFNULL(' . $minAptStatusExpr . ', 999) WHEN 4 THEN 1 WHEN 1 THEN 2 WHEN 3 THEN 3 ELSE 4 END ASC, IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, {$diagTbl}.id DESC";
}
/** 未服务天数锚点:血糖/饮食/运动记录最近 record_date 的最大值 */
private function unservedAnchorExpr(string $diagTbl): string
{
$bloodTbl = (new BloodRecord())->getTable();
$dietTbl = (new DietRecord())->getTable();
$exerciseTbl = (new ExerciseRecord())->getTable();
return 'GREATEST('
. "COALESCE((SELECT MAX(br.record_date) FROM {$bloodTbl} br WHERE br.diagnosis_id = {$diagTbl}.id AND br.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(dr.record_date) FROM {$dietTbl} dr WHERE dr.diagnosis_id = {$diagTbl}.id AND dr.delete_time IS NULL), 0), "
. "COALESCE((SELECT MAX(er.record_date) FROM {$exerciseTbl} er WHERE er.diagnosis_id = {$diagTbl}.id AND er.delete_time IS NULL), 0)"
. ')';
}
private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string private function latestAppointmentIdSubSql(string $aptTbl, string $diagTbl): string
{ {
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES); $statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);