This commit is contained in:
2026-05-05 16:16:55 +08:00
parent 19bfeef98f
commit 49d0fed14e
8 changed files with 241 additions and 139 deletions
+10 -1
View File
@@ -5,11 +5,20 @@ export function tcmDiagnosisLists(params: any) {
return request.get({ url: '/tcm.diagnosis/lists', params }) return request.get({ url: '/tcm.diagnosis/lists', params })
} }
/** 二诊只读病例详情(T1+T2):返回 appointment + diagnosis + tracking + 未服务天数 */ /** 二诊只读病例详情(T1+T2):返回 appointment + diagnosis + 未服务天数(不含跟踪记录) */
export function diagnosisReadonlyDetail(params: { id: number }) { export function diagnosisReadonlyDetail(params: { id: number }) {
return request.get({ url: '/tcm.diagnosis/readonlyDetail', params }) return request.get({ url: '/tcm.diagnosis/readonlyDetail', params })
} }
/** 跟踪信息(血糖血压 / 饮食 / 运动)按日期区间 lazy load — 由 TrackingMatrix 调用 */
export function diagnosisTrackingWindow(params: {
id: number
start_date?: string
end_date?: string
}) {
return request.get({ url: '/tcm.diagnosis/trackingWindow', params })
}
// 医助理诊单统计(按部门、按人) // 医助理诊单统计(按部门、按人)
export function assistantDiagnosisStats(params?: { export function assistantDiagnosisStats(params?: {
start_time?: string start_time?: string
+7 -66
View File
@@ -126,65 +126,13 @@
/> />
</div> </div>
<!-- 血糖血压记录 --> <!-- 跟踪信息与只读病例页同口径自治 lazy load -->
<div class="card metric-card"> <div class="card metric-card">
<div class="card-title">血糖血压记录</div> <div class="card-title">跟踪信息</div>
<div class="metric-viewport"> <TrackingMatrix
<el-table :diagnosis-id="Number(diag?.id ?? 0)"
:data="bloodRecords" :age="diag?.age ?? null"
size="small" />
stripe
empty-text="暂无血糖血压记录"
class="metric-table"
max-height="360"
>
<el-table-column prop="record_date" label="记录日期" width="110" />
<el-table-column prop="record_time" label="记录时间" width="100">
<template #default="{ row }">{{ row.record_time || '—' }}</template>
</el-table-column>
<el-table-column label="空腹血糖(mmol/L)" width="150">
<template #default="{ row }">
<span class="metric-value" :class="{ high: isHighFastingBloodSugar(row.fasting_blood_sugar, diag.age) }">
{{ row.fasting_blood_sugar ?? '—' }}
<span v-if="isHighFastingBloodSugar(row.fasting_blood_sugar, diag.age)" class="metric-up"></span>
</span>
</template>
</el-table-column>
<el-table-column label="餐后2小时血糖(mmol/L)" width="180">
<template #default="{ row }">
<span class="metric-value" :class="{ high: isHighPostprandialBloodSugar(row.postprandial_blood_sugar, diag.age) }">
{{ row.postprandial_blood_sugar ?? '—' }}
<span v-if="isHighPostprandialBloodSugar(row.postprandial_blood_sugar, diag.age)" class="metric-up"></span>
</span>
</template>
</el-table-column>
<el-table-column label="其他血糖(mmol/L)" width="150">
<template #default="{ row }">
<span class="metric-value" :class="{ high: isHighOtherBloodSugar(row.other_blood_sugar, diag.age) }">
{{ row.other_blood_sugar ?? '—' }}
<span v-if="isHighOtherBloodSugar(row.other_blood_sugar, diag.age)" class="metric-up"></span>
</span>
</template>
</el-table-column>
<el-table-column label="血压(mmHg)" width="130">
<template #default="{ row }">
<span class="metric-value" :class="{ high: isHighBloodPressure(row) }">
{{ formatBp(row) }}
<span v-if="isHighBloodPressure(row)" class="metric-up"></span>
</span>
</template>
</el-table-column>
<el-table-column label="西药" width="120">
<template #default="{ row }">{{ row.western_medicine || '—' }}</template>
</el-table-column>
<el-table-column label="胰岛素" width="120">
<template #default="{ row }">{{ row.insulin || '—' }}</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
</el-table>
</div>
</div> </div>
</template> </template>
</section> </section>
@@ -242,18 +190,12 @@ import { receptionQueue, receptionDetail, notifyAssistant, addDoctorNote } from
import NoteTimeline from './components/NoteTimeline.vue' import NoteTimeline from './components/NoteTimeline.vue'
import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.vue' import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.vue'
import PatientCaseCard from '@/views/tcm/diagnosis/components/PatientCaseCard.vue' import PatientCaseCard from '@/views/tcm/diagnosis/components/PatientCaseCard.vue'
import TrackingMatrix from '@/views/tcm/diagnosis/components/TrackingMatrix.vue'
import { completeAppointment } from '@/api/doctor' import { completeAppointment } from '@/api/doctor'
import { getCallSignature } from '@/api/tcm' import { getCallSignature } from '@/api/tcm'
import feedback from '@/utils/feedback' import feedback from '@/utils/feedback'
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display' import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
import { formatGender } from '@/utils/diag-display' import { formatGender } from '@/utils/diag-display'
import {
isHighFastingBloodSugar,
isHighPostprandialBloodSugar,
isHighOtherBloodSugar,
isHighBloodPressure,
formatBp
} from '@/utils/blood-thresholds'
import { import {
Phone, Phone,
Search, Search,
@@ -323,7 +265,6 @@ const noteSaving = ref(false)
const apt = computed<any>(() => detail.value?.appointment || {}) const apt = computed<any>(() => detail.value?.appointment || {})
const diag = computed<any>(() => detail.value?.diagnosis || {}) const diag = computed<any>(() => detail.value?.diagnosis || {})
const bloodRecords = computed<any[]>(() => detail.value?.blood_records || [])
const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || []) const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || [])
const todayMeta = computed(() => { const todayMeta = computed(() => {
@@ -1,16 +1,34 @@
<template> <template>
<div class="tracking-matrix"> <div class="tracking-matrix" v-loading="loading">
<div class="toolbar"> <div class="toolbar">
<el-radio-group v-model="windowDays" @change="onWindowChange"> <el-radio-group v-model="rangeMode" @change="onRangeModeChange">
<el-radio-button :value="7">最近 7 </el-radio-button> <el-radio-button value="7">最近 7 </el-radio-button>
<el-radio-button :value="30">最近 30 </el-radio-button> <el-radio-button value="30">最近 30 </el-radio-button>
<el-radio-button value="custom">自定义</el-radio-button>
</el-radio-group> </el-radio-group>
<span class="hint">同一天血糖三段挤一格超阈值红字年龄分段&lt;50空7/餐950空8/餐10</span>
<el-date-picker
v-if="rangeMode === 'custom'"
v-model="customRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
:clearable="false"
size="default"
:disabled-date="disabledFutureDate"
@change="onCustomRangeChange"
/>
<span class="hint">
超阈值红字年龄分段&lt;50空7/餐950空8/餐10
</span>
</div> </div>
<el-empty <el-empty
v-if="visibleDates.length === 0" v-if="!loading && visibleDates.length === 0"
description="近期无任何打卡记录" description="所选区间内无任何打卡记录"
:image-size="80" :image-size="80"
/> />
@@ -112,7 +130,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { import {
isHighFastingBloodSugar, isHighFastingBloodSugar,
isHighPostprandialBloodSugar, isHighPostprandialBloodSugar,
@@ -120,6 +138,7 @@ import {
isHighBloodPressure, isHighBloodPressure,
formatBp formatBp
} from '@/utils/blood-thresholds' } from '@/utils/blood-thresholds'
import { diagnosisTrackingWindow } from '@/api/tcm'
interface BloodRecord { interface BloodRecord {
record_date?: string record_date?: string
@@ -136,7 +155,6 @@ interface BloodRecord {
interface DietRecord { interface DietRecord {
record_date?: string record_date?: string
record_date_ts?: number
breakfast?: string breakfast?: string
lunch?: string lunch?: string
dinner?: string dinner?: string
@@ -145,7 +163,6 @@ interface DietRecord {
interface ExerciseRecord { interface ExerciseRecord {
record_date?: string record_date?: string
record_date_ts?: number
duration?: number duration?: number
intensity?: number intensity?: number
intensity_text?: string intensity_text?: string
@@ -154,35 +171,112 @@ interface ExerciseRecord {
} }
interface Props { interface Props {
diagnosisId?: number diagnosisId: number
patientId?: number | null
age?: number | null age?: number | null
bloodRecords: BloodRecord[]
dietRecords: DietRecord[]
exerciseRecords: ExerciseRecord[]
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
diagnosisId: 0, age: null
patientId: null,
age: null,
bloodRecords: () => [],
dietRecords: () => [],
exerciseRecords: () => []
}) })
const windowDays = ref<7 | 30>(30) type RangeMode = '7' | '30' | 'custom'
const rangeMode = ref<RangeMode>('30')
const customRange = ref<[string, string] | null>(null)
const onWindowChange = () => { const loading = ref(false)
// 触发 computed 重算;预留扩展点:未来可在此处通知父组件重拉 const bloodRecords = ref<BloodRecord[]>([])
const dietRecords = ref<DietRecord[]>([])
const exerciseRecords = ref<ExerciseRecord[]>([])
let requestSeq = 0
/** 把今天本地时间格式成 Y-m-d,避免 toISOString 的时区漂移 */
const formatDate = (d: Date): string =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
const computeRange = (mode: RangeMode): { start: string; end: string } => {
if (mode === 'custom') {
const [s, e] = customRange.value || ['', '']
return { start: s || '', end: e || '' }
}
const days = mode === '7' ? 7 : 30
const end = new Date()
end.setHours(0, 0, 0, 0)
const start = new Date(end)
start.setDate(start.getDate() - (days - 1))
return { start: formatDate(start), end: formatDate(end) }
} }
const disabledFutureDate = (date: Date) => date.getTime() > Date.now()
const fetchTracking = async () => {
if (!props.diagnosisId || props.diagnosisId <= 0) {
bloodRecords.value = []
dietRecords.value = []
exerciseRecords.value = []
return
}
const range = computeRange(rangeMode.value)
if (rangeMode.value === 'custom' && (!range.start || !range.end)) {
// 等待用户选完日期
return
}
const seq = ++requestSeq
loading.value = true
try {
const res: any = await diagnosisTrackingWindow({
id: props.diagnosisId,
start_date: range.start,
end_date: range.end
})
if (seq !== requestSeq) return
bloodRecords.value = res?.blood_records || []
dietRecords.value = res?.diet_records || []
exerciseRecords.value = res?.exercise_records || []
} catch {
if (seq === requestSeq) {
bloodRecords.value = []
dietRecords.value = []
exerciseRecords.value = []
}
} finally {
if (seq === requestSeq) loading.value = false
}
}
const onRangeModeChange = () => {
if (rangeMode.value === 'custom') {
// 默认填入「最近 30 天」作为初值,让用户在此基础上调整
if (!customRange.value) {
const r = computeRange('30')
customRange.value = [r.start, r.end]
}
}
fetchTracking()
}
const onCustomRangeChange = () => {
if (rangeMode.value === 'custom') fetchTracking()
}
watch(
() => props.diagnosisId,
(id) => {
if (id && id > 0) fetchTracking()
}
)
onMounted(() => {
if (props.diagnosisId && props.diagnosisId > 0) fetchTracking()
})
/** /**
* 把同一天可能多条的血糖记录聚合:每段取第一条非空值(与 BloodRecordLogic 趋势图一致) * 把同一天可能多条的血糖记录聚合:每段取第一条非空值(与 BloodRecordLogic 趋势图一致)
*/ */
const bloodByDate = computed(() => { const bloodByDate = computed(() => {
const map = new Map<string, BloodRecord>() const map = new Map<string, BloodRecord>()
for (const r of props.bloodRecords || []) { for (const r of bloodRecords.value) {
const d = String(r.record_date || '') const d = String(r.record_date || '')
if (!d) continue if (!d) continue
if (!map.has(d)) { if (!map.has(d)) {
@@ -202,7 +296,12 @@ const bloodByDate = computed(() => {
'insulin' 'insulin'
] ]
for (const f of fields) { for (const f of fields) {
if ((acc[f] === undefined || acc[f] === null || acc[f] === '') && r[f] !== undefined && r[f] !== null && r[f] !== '') { if (
(acc[f] === undefined || acc[f] === null || acc[f] === '') &&
r[f] !== undefined &&
r[f] !== null &&
r[f] !== ''
) {
;(acc as any)[f] = r[f] ;(acc as any)[f] = r[f]
} }
} }
@@ -212,7 +311,7 @@ const bloodByDate = computed(() => {
const dietByDate = computed(() => { const dietByDate = computed(() => {
const map = new Map<string, DietRecord>() const map = new Map<string, DietRecord>()
for (const r of props.dietRecords || []) { for (const r of dietRecords.value) {
const d = String(r.record_date || '') const d = String(r.record_date || '')
if (!d) continue if (!d) continue
if (!map.has(d)) map.set(d, r) if (!map.has(d)) map.set(d, r)
@@ -222,7 +321,7 @@ const dietByDate = computed(() => {
const exerciseByDate = computed(() => { const exerciseByDate = computed(() => {
const map = new Map<string, ExerciseRecord>() const map = new Map<string, ExerciseRecord>()
for (const r of props.exerciseRecords || []) { for (const r of exerciseRecords.value) {
const d = String(r.record_date || '') const d = String(r.record_date || '')
if (!d) continue if (!d) continue
if (!map.has(d)) map.set(d, r) if (!map.has(d)) map.set(d, r)
@@ -231,24 +330,18 @@ const exerciseByDate = computed(() => {
}) })
/** /**
* 取三类记录的日期并集,按 desc 排序并去重,限制最大 windowDays 条 * 已经由后端按区间过滤,前端只需取三类记录的日期并集 + desc 排序
*/ */
const visibleDates = computed<string[]>(() => { const visibleDates = computed<string[]>(() => {
const set = new Set<string>() const set = new Set<string>()
for (const r of props.bloodRecords || []) if (r.record_date) set.add(String(r.record_date)) for (const r of bloodRecords.value) if (r.record_date) set.add(String(r.record_date))
for (const r of props.dietRecords || []) if (r.record_date) set.add(String(r.record_date)) for (const r of dietRecords.value) if (r.record_date) set.add(String(r.record_date))
for (const r of props.exerciseRecords || []) if (r.record_date) set.add(String(r.record_date)) for (const r of exerciseRecords.value) if (r.record_date) set.add(String(r.record_date))
const list = Array.from(set).filter((d) => d.length === 10) const list = Array.from(set).filter((d) => d.length === 10)
list.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0)) list.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
return list.slice(0, windowDays.value) return list
}) })
const weekText = (d: string) => {
const ts = Date.parse(d + 'T00:00:00')
if (!Number.isFinite(ts)) return ''
const w = new Date(ts).getDay()
return ['日', '一', '二', '三', '四', '五', '六'][w]
}
const rows = computed(() => const rows = computed(() =>
visibleDates.value.map((date) => { visibleDates.value.map((date) => {
@@ -263,15 +356,15 @@ const rows = computed(() =>
const dia = blood?.diastolic_pressure ?? null const dia = blood?.diastolic_pressure ?? null
const bloodAny = const bloodAny =
fasting !== null && fasting !== '' || (fasting !== null && fasting !== '') ||
postprandial !== null && postprandial !== '' || (postprandial !== null && postprandial !== '') ||
other !== null && other !== '' (other !== null && other !== '')
const bp = sys || dia ? formatBp({ systolic_pressure: sys, diastolic_pressure: dia }) : '' const bp = sys || dia ? formatBp({ systolic_pressure: sys, diastolic_pressure: dia }) : ''
return { return {
date, date,
dateLabel: `${date.slice(5)}(周${weekText(date)}`, dateLabel: `${date}`,
// 血糖 // 血糖
bloodAny, bloodAny,
fasting: fasting ?? '—', fasting: fasting ?? '—',
@@ -305,6 +398,8 @@ const rows = computed(() =>
} }
}) })
) )
defineExpose({ refresh: fetchTracking })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+1 -8
View File
@@ -41,14 +41,10 @@
<!-- 跟踪信息 --> <!-- 跟踪信息 -->
<div class="card tracking-card"> <div class="card tracking-card">
<div class="card-title">跟踪信息最近 30 </div> <div class="card-title">跟踪信息</div>
<TrackingMatrix <TrackingMatrix
:diagnosis-id="Number(diagnosisIdNum)" :diagnosis-id="Number(diagnosisIdNum)"
:patient-id="Number(diag?.patient_id ?? 0)"
:age="diag?.age ?? null" :age="diag?.age ?? null"
:blood-records="bloodRecords"
:diet-records="dietRecords"
:exercise-records="exerciseRecords"
/> />
</div> </div>
@@ -83,9 +79,6 @@ const detail = ref<any>(null)
const apt = computed<any>(() => detail.value?.appointment || {}) const apt = computed<any>(() => detail.value?.appointment || {})
const diag = computed<any>(() => detail.value?.diagnosis || {}) const diag = computed<any>(() => detail.value?.diagnosis || {})
const bloodRecords = computed<any[]>(() => detail.value?.blood_records || [])
const dietRecords = computed<any[]>(() => detail.value?.diet_records || [])
const exerciseRecords = computed<any[]>(() => detail.value?.exercise_records || [])
const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || []) const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || [])
const unservedDays = computed<number | null>(() => { const unservedDays = computed<number | null>(() => {
const v = detail.value?.unserved_days const v = detail.value?.unserved_days
@@ -138,6 +138,25 @@ class DiagnosisController extends BaseAdminController
return $this->data($result); return $this->data($result);
} }
/**
* @notes 跟踪信息(血糖血压 / 饮食 / 运动)—— 按日期区间 lazy load
*
* 路由:GET /tcm.diagnosis/trackingWindow?id=:diagnosisId&start_date=&end_date=
* 权限:tcm.diagnosis/readonlyDetail(与只读病例页同档;接诊台亦复用)
*
* @return \think\response\Json
*/
public function trackingWindow()
{
$params = (new DiagnosisValidate())->goCheck('trackingWindow');
$result = DiagnosisLogic::fetchTrackingWindow(
(int) $params['id'],
(string) ($params['start_date'] ?? ''),
(string) ($params['end_date'] ?? '')
);
return $this->data($result);
}
/** /**
* @notes 诊单挂号 / 取消挂号 操作日志(谁在何时操作) * @notes 诊单挂号 / 取消挂号 操作日志(谁在何时操作)
*/ */
@@ -516,10 +516,8 @@ class AppointmentLogic extends BaseLogic
? DiagnosisLogic::detail(['id' => $diagnosisId]) ? DiagnosisLogic::detail(['id' => $diagnosisId])
: []; : [];
// 3) 血糖血压记录 // 3) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
$bloodRecords = $diagnosisId > 0 // 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
? BloodRecordLogic::getRecordsByPatient(['diagnosis_id' => $diagnosisId])
: [];
// 4) 补全医助姓名(detail() 未关联 admin 助理表) // 4) 补全医助姓名(detail() 未关联 admin 助理表)
$assistantName = ''; $assistantName = '';
@@ -554,7 +552,6 @@ class AppointmentLogic extends BaseLogic
return [ return [
'appointment' => $appointment, 'appointment' => $appointment,
'diagnosis' => $diagnosis, 'diagnosis' => $diagnosis,
'blood_records' => $bloodRecords,
'doctor_notes' => $doctorNotes, 'doctor_notes' => $doctorNotes,
]; ];
} }
@@ -3397,14 +3397,8 @@ class DiagnosisLogic extends BaseLogic
// 3) 最近一条挂号(PatientInfoCard 显示用,无挂号则给默认空对象) // 3) 最近一条挂号(PatientInfoCard 显示用,无挂号则给默认空对象)
$appointment = self::buildLatestAppointmentForReadonly((int) ($diagnosis['id'] ?? $diagnosisId), $diagnosis); $appointment = self::buildLatestAppointmentForReadonly((int) ($diagnosis['id'] ?? $diagnosisId), $diagnosis);
// 4) 最近 30 天的跟踪记录(一次性 join 出三类,前端透视为矩阵) // 4) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
$windowDays = 30; // 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
$sinceTs = strtotime('-' . $windowDays . ' days');
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
$bloodRecords = self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs);
$dietRecords = self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs);
$exerciseRecords = self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs);
// 5) 医生备注 // 5) 医生备注
$doctorNotes = DoctorNoteLogic::getByDiagnosis($diagnosisId); $doctorNotes = DoctorNoteLogic::getByDiagnosis($diagnosisId);
@@ -3419,13 +3413,39 @@ class DiagnosisLogic extends BaseLogic
return [ return [
'appointment' => $appointment, 'appointment' => $appointment,
'diagnosis' => $diagnosis, 'diagnosis' => $diagnosis,
'blood_records' => $bloodRecords,
'diet_records' => $dietRecords,
'exercise_records' => $exerciseRecords,
'doctor_notes' => $doctorNotes, 'doctor_notes' => $doctorNotes,
'unserved_days' => $unservedDays, 'unserved_days' => $unservedDays,
'last_blood_record_at' => $lastBloodRecordAt, 'last_blood_record_at' => $lastBloodRecordAt,
'window_days' => $windowDays, ];
}
/**
* 取指定日期区间内的三类跟踪记录(血糖血压 / 饮食 / 运动),供 readonlyDetail 与
* 医生接诊台 reception 通过独立接口 lazy load。
*
* 区间语义:闭区间 [startDate, endDate]Y-m-d),均不传则不限。
*
* @return array{
* blood_records: array<int,array<string,mixed>>,
* diet_records: array<int,array<string,mixed>>,
* exercise_records: array<int,array<string,mixed>>,
* start_date: string,
* end_date: string,
* }
*/
public static function fetchTrackingWindow(int $diagnosisId, string $startDate = '', string $endDate = ''): array
{
$sinceTs = $startDate !== '' ? (int) strtotime($startDate . ' 00:00:00') : 0;
$untilTs = $endDate !== '' ? (int) strtotime($endDate . ' 23:59:59') : 0;
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
$untilTs = $untilTs > 0 ? $untilTs : 0;
return [
'blood_records' => self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
'diet_records' => self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
'exercise_records' => self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs, $untilTs),
'start_date' => $startDate,
'end_date' => $endDate,
]; ];
} }
@@ -3502,12 +3522,15 @@ class DiagnosisLogic extends BaseLogic
/** /**
* @return array<int,array<string,mixed>> * @return array<int,array<string,mixed>>
*/ */
private static function fetchBloodRecordsForReadonly(int $diagnosisId, int $sinceTs): array private static function fetchBloodRecordsForReadonly(int $diagnosisId, int $sinceTs, int $untilTs = 0): array
{ {
$query = BloodRecord::where('diagnosis_id', $diagnosisId); $query = BloodRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) { if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs); $query->where('record_date', '>=', $sinceTs);
} }
if ($untilTs > 0) {
$query->where('record_date', '<=', $untilTs);
}
$rows = $query->order('record_date', 'desc') $rows = $query->order('record_date', 'desc')
->order('record_time', 'desc') ->order('record_time', 'desc')
->select() ->select()
@@ -3526,12 +3549,15 @@ class DiagnosisLogic extends BaseLogic
/** /**
* @return array<int,array<string,mixed>> * @return array<int,array<string,mixed>>
*/ */
private static function fetchDietRecordsForReadonly(int $diagnosisId, int $sinceTs): array private static function fetchDietRecordsForReadonly(int $diagnosisId, int $sinceTs, int $untilTs = 0): array
{ {
$query = DietRecord::where('diagnosis_id', $diagnosisId); $query = DietRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) { if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs); $query->where('record_date', '>=', $sinceTs);
} }
if ($untilTs > 0) {
$query->where('record_date', '<=', $untilTs);
}
$rows = $query->order('record_date', 'desc')->select()->toArray(); $rows = $query->order('record_date', 'desc')->select()->toArray();
foreach ($rows as &$row) { foreach ($rows as &$row) {
@@ -3546,12 +3572,15 @@ class DiagnosisLogic extends BaseLogic
/** /**
* @return array<int,array<string,mixed>> * @return array<int,array<string,mixed>>
*/ */
private static function fetchExerciseRecordsForReadonly(int $diagnosisId, int $sinceTs): array private static function fetchExerciseRecordsForReadonly(int $diagnosisId, int $sinceTs, int $untilTs = 0): array
{ {
$query = ExerciseRecord::where('diagnosis_id', $diagnosisId); $query = ExerciseRecord::where('diagnosis_id', $diagnosisId);
if ($sinceTs > 0) { if ($sinceTs > 0) {
$query->where('record_date', '>=', $sinceTs); $query->where('record_date', '>=', $sinceTs);
} }
if ($untilTs > 0) {
$query->where('record_date', '<=', $untilTs);
}
$rows = $query->order('record_date', 'desc') $rows = $query->order('record_date', 'desc')
->append(['intensity_text']) ->append(['intensity_text'])
->select() ->select()
@@ -37,6 +37,8 @@ class DiagnosisValidate extends BaseValidate
'tongue_images' => 'array', 'tongue_images' => 'array',
'report_files' => 'array', 'report_files' => 'array',
'diabetes_discovery_year' => 'max:50', 'diabetes_discovery_year' => 'max:50',
'start_date' => 'date',
'end_date' => 'date|checkDateRange',
]; ];
protected $message = [ protected $message = [
@@ -55,6 +57,8 @@ class DiagnosisValidate extends BaseValidate
'status.in' => '状态参数错误', 'status.in' => '状态参数错误',
'current_medications.max' => '在用药物最多2000个字符', 'current_medications.max' => '在用药物最多2000个字符',
'diabetes_discovery_year.max' => '发现糖尿病患病史最多50个字符', 'diabetes_discovery_year.max' => '发现糖尿病患病史最多50个字符',
'start_date.date' => '开始日期格式不正确',
'end_date.date' => '结束日期格式不正确',
]; ];
public function sceneAdd() public function sceneAdd()
@@ -76,7 +80,12 @@ class DiagnosisValidate extends BaseValidate
{ {
return $this->only(['id']); return $this->only(['id']);
} }
public function sceneTrackingWindow()
{
return $this->only(['id', 'start_date', 'end_date']);
}
public function sceneGenerateQrcode() public function sceneGenerateQrcode()
{ {
return $this->only(['diagnosis_id', 'doctor_id', 'patient_id', 'share_user_id', 'mini_program_path']) return $this->only(['diagnosis_id', 'doctor_id', 'patient_id', 'share_user_id', 'mini_program_path'])
@@ -112,6 +121,16 @@ class DiagnosisValidate extends BaseValidate
return true; return true;
} }
/** end_date 不能早于 start_date */
protected function checkDateRange($value, $rule, $data = [])
{
$start = $data['start_date'] ?? '';
if ($start && $value && strtotime($value) < strtotime($start)) {
return '结束日期不能早于开始日期';
}
return true;
}
/** 视频场景需 doctor_id,确认诊单需 diagnosis_id */ /** 视频场景需 doctor_id,确认诊单需 diagnosis_id */
protected function checkQrcodeIds($value, $rule, $data = []) protected function checkQrcodeIds($value, $rule, $data = [])
{ {