diff --git a/admin/src/views/tcm/diagnosis/index.vue b/admin/src/views/tcm/diagnosis/index.vue
index d0ed87b5..65ee9e62 100644
--- a/admin/src/views/tcm/diagnosis/index.vue
+++ b/admin/src/views/tcm/diagnosis/index.vue
@@ -121,6 +121,16 @@
end-placeholder="最近挂号结束"
@change="handleLatestAppointmentFilterChange"
/>
+
未开方
-
+
{
- return buildTcmDiagnosisListRequestPayload({
- ...formData,
+/** 除顶部 Tab 专属条件外,与主列表共用的「更多筛选」参数(角标 count 需同步) */
+function buildSharedDiagnosisFilterPayload(): Record {
+ return {
+ 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 = {}): Record {
+ return {
page_no: 1,
page_size: 1,
- pending_assign: 1,
+ ...buildSharedDiagnosisFilterPayload(),
appointment_date: '',
- has_appointment: '',
pending_booking: '',
completed_appointment: '',
- latest_appointment_start_date: '',
- latest_appointment_end_date: '',
- latest_appointment_channel_source: '',
- pending_assign_order_month: resolvePendingAssignOrderMonthForRequest()
- } as Record) as Record
+ pending_assign: '',
+ pending_assign_order_month: '',
+ pending_assign_keyword: '',
+ sort_unserved_days: '',
+ ...overrides
+ }
+}
+
+/** 待分配角标 count 请求:与列表同条件,且去掉其它顶部 Tab 残留(如默认「当天挂号」) */
+function buildPendingAssignCountPayload(): Record {
+ return buildTcmDiagnosisListRequestPayload(
+ buildDateCountRequestPayload({
+ pending_assign: 1,
+ has_appointment: '',
+ pending_assign_order_month: resolvePendingAssignOrderMonthForRequest(),
+ pending_assign_keyword: formData.pending_assign_keyword
+ }) as Record
+ ) as Record
}
const fetchTcmDiagnosisListsForPaging = (req: Record) =>
@@ -893,6 +943,9 @@ function clearSecondaryFiltersWhenPendingAssignWideSearch() {
formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = ''
+ formData.latest_assign_start_date = ''
+ formData.latest_assign_end_date = ''
+ formData.sort_unserved_days = ''
formData.pending_assign_order_month = ''
activeTab.value = 'all'
if (kw1 !== '') {
@@ -994,14 +1047,26 @@ const onPendingAssignOrderMonthChange = async (val: string | null) => {
const fetchDateCounts = async () => {
try {
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({ appointment_date: dayBeforeStr.value, page_no: 1, page_size: 1 }),
- tcmDiagnosisLists({ appointment_date: todayStr.value, page_no: 1, page_size: 1 }),
- tcmDiagnosisLists({ appointment_date: tomorrowStr.value, page_no: 1, page_size: 1 }),
- tcmDiagnosisLists({ appointment_date: dayAfterStr.value, page_no: 1, page_size: 1 }),
- 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(
+ buildDateCountRequestPayload({ appointment_date: yesterdayStr.value, has_appointment: '' }) as any
+ ),
+ tcmDiagnosisLists(
+ buildDateCountRequestPayload({ appointment_date: dayBeforeStr.value, has_appointment: '' }) as any
+ ),
+ tcmDiagnosisLists(
+ 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)
])
dateCounts.value = {
@@ -1135,6 +1200,11 @@ const clearLatestAppointmentFilters = () => {
formData.latest_appointment_channel_source = ''
}
+const clearLatestAssignFilters = () => {
+ formData.latest_assign_start_date = ''
+ formData.latest_assign_end_date = ''
+}
+
const hasLatestAppointmentFilter = () =>
!!(
formData.latest_appointment_start_date ||
@@ -1142,6 +1212,9 @@ const hasLatestAppointmentFilter = () =>
formData.latest_appointment_channel_source
)
+const hasLatestAssignFilter = () =>
+ !!(formData.latest_assign_start_date || formData.latest_assign_end_date)
+
const handleLatestAppointmentFilterChange = () => {
if (hasLatestAppointmentFilter()) {
formData.appointment_date = ''
@@ -1154,6 +1227,27 @@ const handleLatestAppointmentFilterChange = () => {
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 desc = String(row?.latest_appointment_channel_source_desc || '').trim()
const raw = String(row?.latest_appointment_channel_source || '').trim()
@@ -1200,6 +1294,9 @@ const handleReset = () => {
formData.latest_appointment_start_date = ''
formData.latest_appointment_end_date = ''
formData.latest_appointment_channel_source = ''
+ formData.latest_assign_start_date = ''
+ formData.latest_assign_end_date = ''
+ formData.sort_unserved_days = ''
formData.diagnosis_confirmed = ''
formData.appointment_date = ''
formData.has_appointment = ''
@@ -2298,6 +2395,11 @@ onUnmounted(() => {
max-width: 100%;
}
+ .latest-assign-range {
+ width: 260px;
+ max-width: 100%;
+ }
+
.latest-appointment-channel {
width: 170px;
}
diff --git a/server/app/adminapi/lists/tcm/DiagnosisLists.php b/server/app/adminapi/lists/tcm/DiagnosisLists.php
index f94b9ec2..1e79fab4 100755
--- a/server/app/adminapi/lists/tcm/DiagnosisLists.php
+++ b/server/app/adminapi/lists/tcm/DiagnosisLists.php
@@ -140,6 +140,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
+ $this->applyLatestAssignFilters($query, $pendingWideSearch);
$this->applyPendingAssignBusinessOrderMonthFilter($query);
@@ -167,30 +168,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
}
- // 按挂号状态优先级排序:已过号(4) > 已预约(1) > 已完成(3),然后按挂号日期+时间升序
- // 若传了 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";
- }
+ $orderRaw = $this->resolveListOrderRaw($pendingWideSearch);
$lists = $query
->with(['DiagnosisViewRecord'])
@@ -571,6 +549,7 @@ class DiagnosisLists extends BaseAdminDataLists implements ListsSearchInterface
}
$this->applyLatestAppointmentFilters($query, $pendingWideSearch);
+ $this->applyLatestAssignFilters($query, $pendingWideSearch);
// 仅已开方(待分配+关键词检索时不限制)
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));
}
+ /**
+ * 最近一次成功指派过滤:按 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
{
$statuses = implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES);