1
This commit is contained in:
+10
-1
@@ -5,11 +5,20 @@ export function tcmDiagnosisLists(params: any) {
|
||||
return request.get({ url: '/tcm.diagnosis/lists', params })
|
||||
}
|
||||
|
||||
/** 二诊只读病例详情(T1+T2):返回 appointment + diagnosis + tracking + 未服务天数 */
|
||||
/** 二诊只读病例详情(T1+T2):返回 appointment + diagnosis + 未服务天数(不含跟踪记录) */
|
||||
export function diagnosisReadonlyDetail(params: { id: number }) {
|
||||
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?: {
|
||||
start_time?: string
|
||||
|
||||
@@ -126,65 +126,13 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 血糖血压记录 -->
|
||||
<!-- 跟踪信息(与只读病例页同口径,自治 lazy load) -->
|
||||
<div class="card metric-card">
|
||||
<div class="card-title">血糖血压记录</div>
|
||||
<div class="metric-viewport">
|
||||
<el-table
|
||||
:data="bloodRecords"
|
||||
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 class="card-title">跟踪信息</div>
|
||||
<TrackingMatrix
|
||||
:diagnosis-id="Number(diag?.id ?? 0)"
|
||||
:age="diag?.age ?? null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
@@ -242,18 +190,12 @@ import { receptionQueue, receptionDetail, notifyAssistant, addDoctorNote } from
|
||||
import NoteTimeline from './components/NoteTimeline.vue'
|
||||
import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.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 { getCallSignature } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
||||
import { formatGender } from '@/utils/diag-display'
|
||||
import {
|
||||
isHighFastingBloodSugar,
|
||||
isHighPostprandialBloodSugar,
|
||||
isHighOtherBloodSugar,
|
||||
isHighBloodPressure,
|
||||
formatBp
|
||||
} from '@/utils/blood-thresholds'
|
||||
import {
|
||||
Phone,
|
||||
Search,
|
||||
@@ -323,7 +265,6 @@ const noteSaving = ref(false)
|
||||
|
||||
const apt = computed<any>(() => detail.value?.appointment || {})
|
||||
const diag = computed<any>(() => detail.value?.diagnosis || {})
|
||||
const bloodRecords = computed<any[]>(() => detail.value?.blood_records || [])
|
||||
const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || [])
|
||||
|
||||
const todayMeta = computed(() => {
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
<template>
|
||||
<div class="tracking-matrix">
|
||||
<div class="tracking-matrix" v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<el-radio-group v-model="windowDays" @change="onWindowChange">
|
||||
<el-radio-button :value="7">最近 7 天</el-radio-button>
|
||||
<el-radio-button :value="30">最近 30 天</el-radio-button>
|
||||
<el-radio-group v-model="rangeMode" @change="onRangeModeChange">
|
||||
<el-radio-button value="7">最近 7 天</el-radio-button>
|
||||
<el-radio-button value="30">最近 30 天</el-radio-button>
|
||||
<el-radio-button value="custom">自定义</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="hint">同一天血糖三段挤一格;超阈值红字↑(年龄分段:<50:空7/餐9,≥50:空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">
|
||||
超阈值红字↑(年龄分段:<50:空7/餐9,≥50:空8/餐10)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="visibleDates.length === 0"
|
||||
description="近期无任何打卡记录"
|
||||
v-if="!loading && visibleDates.length === 0"
|
||||
description="所选区间内无任何打卡记录"
|
||||
:image-size="80"
|
||||
/>
|
||||
|
||||
@@ -112,7 +130,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
isHighFastingBloodSugar,
|
||||
isHighPostprandialBloodSugar,
|
||||
@@ -120,6 +138,7 @@ import {
|
||||
isHighBloodPressure,
|
||||
formatBp
|
||||
} from '@/utils/blood-thresholds'
|
||||
import { diagnosisTrackingWindow } from '@/api/tcm'
|
||||
|
||||
interface BloodRecord {
|
||||
record_date?: string
|
||||
@@ -136,7 +155,6 @@ interface BloodRecord {
|
||||
|
||||
interface DietRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
breakfast?: string
|
||||
lunch?: string
|
||||
dinner?: string
|
||||
@@ -145,7 +163,6 @@ interface DietRecord {
|
||||
|
||||
interface ExerciseRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
duration?: number
|
||||
intensity?: number
|
||||
intensity_text?: string
|
||||
@@ -154,35 +171,112 @@ interface ExerciseRecord {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
diagnosisId?: number
|
||||
patientId?: number | null
|
||||
diagnosisId: number
|
||||
age?: number | null
|
||||
bloodRecords: BloodRecord[]
|
||||
dietRecords: DietRecord[]
|
||||
exerciseRecords: ExerciseRecord[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
diagnosisId: 0,
|
||||
patientId: null,
|
||||
age: null,
|
||||
bloodRecords: () => [],
|
||||
dietRecords: () => [],
|
||||
exerciseRecords: () => []
|
||||
age: null
|
||||
})
|
||||
|
||||
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 = () => {
|
||||
// 触发 computed 重算;预留扩展点:未来可在此处通知父组件重拉
|
||||
const loading = ref(false)
|
||||
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 趋势图一致)
|
||||
*/
|
||||
const bloodByDate = computed(() => {
|
||||
const map = new Map<string, BloodRecord>()
|
||||
for (const r of props.bloodRecords || []) {
|
||||
for (const r of bloodRecords.value) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) {
|
||||
@@ -202,7 +296,12 @@ const bloodByDate = computed(() => {
|
||||
'insulin'
|
||||
]
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -212,7 +311,7 @@ const bloodByDate = computed(() => {
|
||||
|
||||
const dietByDate = computed(() => {
|
||||
const map = new Map<string, DietRecord>()
|
||||
for (const r of props.dietRecords || []) {
|
||||
for (const r of dietRecords.value) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
@@ -222,7 +321,7 @@ const dietByDate = computed(() => {
|
||||
|
||||
const exerciseByDate = computed(() => {
|
||||
const map = new Map<string, ExerciseRecord>()
|
||||
for (const r of props.exerciseRecords || []) {
|
||||
for (const r of exerciseRecords.value) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
@@ -231,24 +330,18 @@ const exerciseByDate = computed(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* 取三类记录的日期并集,按 desc 排序并去重,限制最大 windowDays 条
|
||||
* 已经由后端按区间过滤,前端只需取三类记录的日期并集 + desc 排序
|
||||
*/
|
||||
const visibleDates = computed<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 props.dietRecords || []) 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 bloodRecords.value) 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 exerciseRecords.value) if (r.record_date) set.add(String(r.record_date))
|
||||
const list = Array.from(set).filter((d) => d.length === 10)
|
||||
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(() =>
|
||||
visibleDates.value.map((date) => {
|
||||
@@ -263,15 +356,15 @@ const rows = computed(() =>
|
||||
const dia = blood?.diastolic_pressure ?? null
|
||||
|
||||
const bloodAny =
|
||||
fasting !== null && fasting !== '' ||
|
||||
postprandial !== null && postprandial !== '' ||
|
||||
other !== null && other !== ''
|
||||
(fasting !== null && fasting !== '') ||
|
||||
(postprandial !== null && postprandial !== '') ||
|
||||
(other !== null && other !== '')
|
||||
|
||||
const bp = sys || dia ? formatBp({ systolic_pressure: sys, diastolic_pressure: dia }) : ''
|
||||
|
||||
return {
|
||||
date,
|
||||
dateLabel: `${date.slice(5)}(周${weekText(date)})`,
|
||||
dateLabel: `${date}`,
|
||||
// 血糖
|
||||
bloodAny,
|
||||
fasting: fasting ?? '—',
|
||||
@@ -305,6 +398,8 @@ const rows = computed(() =>
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
defineExpose({ refresh: fetchTracking })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -41,14 +41,10 @@
|
||||
|
||||
<!-- 跟踪信息 -->
|
||||
<div class="card tracking-card">
|
||||
<div class="card-title">跟踪信息(最近 30 天)</div>
|
||||
<div class="card-title">跟踪信息</div>
|
||||
<TrackingMatrix
|
||||
:diagnosis-id="Number(diagnosisIdNum)"
|
||||
:patient-id="Number(diag?.patient_id ?? 0)"
|
||||
:age="diag?.age ?? null"
|
||||
:blood-records="bloodRecords"
|
||||
:diet-records="dietRecords"
|
||||
:exercise-records="exerciseRecords"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -83,9 +79,6 @@ const detail = ref<any>(null)
|
||||
|
||||
const apt = computed<any>(() => detail.value?.appointment || {})
|
||||
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 unservedDays = computed<number | null>(() => {
|
||||
const v = detail.value?.unserved_days
|
||||
|
||||
@@ -138,6 +138,25 @@ class DiagnosisController extends BaseAdminController
|
||||
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 诊单挂号 / 取消挂号 操作日志(谁在何时操作)
|
||||
*/
|
||||
|
||||
@@ -516,10 +516,8 @@ class AppointmentLogic extends BaseLogic
|
||||
? DiagnosisLogic::detail(['id' => $diagnosisId])
|
||||
: [];
|
||||
|
||||
// 3) 血糖血压记录
|
||||
$bloodRecords = $diagnosisId > 0
|
||||
? BloodRecordLogic::getRecordsByPatient(['diagnosis_id' => $diagnosisId])
|
||||
: [];
|
||||
// 3) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
|
||||
// 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
|
||||
|
||||
// 4) 补全医助姓名(detail() 未关联 admin 助理表)
|
||||
$assistantName = '';
|
||||
@@ -554,7 +552,6 @@ class AppointmentLogic extends BaseLogic
|
||||
return [
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'blood_records' => $bloodRecords,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3397,14 +3397,8 @@ class DiagnosisLogic extends BaseLogic
|
||||
// 3) 最近一条挂号(PatientInfoCard 显示用,无挂号则给默认空对象)
|
||||
$appointment = self::buildLatestAppointmentForReadonly((int) ($diagnosis['id'] ?? $diagnosisId), $diagnosis);
|
||||
|
||||
// 4) 最近 30 天的跟踪记录(一次性 join 出三类,前端透视为矩阵)
|
||||
$windowDays = 30;
|
||||
$sinceTs = strtotime('-' . $windowDays . ' days');
|
||||
$sinceTs = $sinceTs > 0 ? $sinceTs : 0;
|
||||
|
||||
$bloodRecords = self::fetchBloodRecordsForReadonly($diagnosisId, $sinceTs);
|
||||
$dietRecords = self::fetchDietRecordsForReadonly($diagnosisId, $sinceTs);
|
||||
$exerciseRecords = self::fetchExerciseRecordsForReadonly($diagnosisId, $sinceTs);
|
||||
// 4) 跟踪信息(血糖血压 / 饮食 / 运动)改为前端按窗口 lazy load,
|
||||
// 详见 DiagnosisController::trackingWindow。此处不再一次性返回。
|
||||
|
||||
// 5) 医生备注
|
||||
$doctorNotes = DoctorNoteLogic::getByDiagnosis($diagnosisId);
|
||||
@@ -3419,13 +3413,39 @@ class DiagnosisLogic extends BaseLogic
|
||||
return [
|
||||
'appointment' => $appointment,
|
||||
'diagnosis' => $diagnosis,
|
||||
'blood_records' => $bloodRecords,
|
||||
'diet_records' => $dietRecords,
|
||||
'exercise_records' => $exerciseRecords,
|
||||
'doctor_notes' => $doctorNotes,
|
||||
'unserved_days' => $unservedDays,
|
||||
'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>>
|
||||
*/
|
||||
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);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$rows = $query->order('record_date', 'desc')
|
||||
->order('record_time', 'desc')
|
||||
->select()
|
||||
@@ -3526,12 +3549,15 @@ class DiagnosisLogic extends BaseLogic
|
||||
/**
|
||||
* @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);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$rows = $query->order('record_date', 'desc')->select()->toArray();
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
@@ -3546,12 +3572,15 @@ class DiagnosisLogic extends BaseLogic
|
||||
/**
|
||||
* @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);
|
||||
if ($sinceTs > 0) {
|
||||
$query->where('record_date', '>=', $sinceTs);
|
||||
}
|
||||
if ($untilTs > 0) {
|
||||
$query->where('record_date', '<=', $untilTs);
|
||||
}
|
||||
$rows = $query->order('record_date', 'desc')
|
||||
->append(['intensity_text'])
|
||||
->select()
|
||||
|
||||
@@ -37,6 +37,8 @@ class DiagnosisValidate extends BaseValidate
|
||||
'tongue_images' => 'array',
|
||||
'report_files' => 'array',
|
||||
'diabetes_discovery_year' => 'max:50',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date|checkDateRange',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
@@ -55,6 +57,8 @@ class DiagnosisValidate extends BaseValidate
|
||||
'status.in' => '状态参数错误',
|
||||
'current_medications.max' => '在用药物最多2000个字符',
|
||||
'diabetes_discovery_year.max' => '发现糖尿病患病史最多50个字符',
|
||||
'start_date.date' => '开始日期格式不正确',
|
||||
'end_date.date' => '结束日期格式不正确',
|
||||
];
|
||||
|
||||
public function sceneAdd()
|
||||
@@ -76,7 +80,12 @@ class DiagnosisValidate extends BaseValidate
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
|
||||
|
||||
public function sceneTrackingWindow()
|
||||
{
|
||||
return $this->only(['id', 'start_date', 'end_date']);
|
||||
}
|
||||
|
||||
public function sceneGenerateQrcode()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
protected function checkQrcodeIds($value, $rule, $data = [])
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user