新增
This commit is contained in:
+1506
-1506
File diff suppressed because it is too large
Load Diff
+1828
-1828
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,179 +1,179 @@
|
||||
<template>
|
||||
<div class="appointment-record-panel">
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无挂号记录">
|
||||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="
|
||||
row.status === 1
|
||||
? 'success'
|
||||
: row.status === 2
|
||||
? 'info'
|
||||
: row.status === 3
|
||||
? 'primary'
|
||||
: 'danger'
|
||||
"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ row.status_desc || '—' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="患者(挂号人)" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="font-medium">{{ row.patient_name || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ row.patient_phone || '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂号医生" prop="doctor_name" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="挂号助理" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.assistant_name || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时间" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span>{{ row.appointment_date || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ formatTime(row.appointment_time) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时段" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ periodText(row.period) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.appointment_type_desc || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ channelLabel(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="确认诊单" width="92" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.diagnosis_confirmed" type="success" size="small" effect="plain">已确认</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">未确认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开方" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="165" prop="create_time" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { appointmentLists } from '@/api/doctor'
|
||||
import { getDictData } from '@/api/app'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const channelMap = ref<Record<string, string>>({})
|
||||
|
||||
function formatTime(t: string | undefined) {
|
||||
if (!t) return ''
|
||||
return String(t).length >= 8 ? String(t).slice(0, 5) : t
|
||||
}
|
||||
|
||||
function periodText(p: string | undefined) {
|
||||
if (p === 'morning') return '上午'
|
||||
if (p === 'afternoon') return '下午'
|
||||
if (p === 'all') return '全天'
|
||||
return p || '—'
|
||||
}
|
||||
|
||||
function channelLabel(row: Record<string, any>) {
|
||||
const raw = row.channel_source ?? row.channels ?? ''
|
||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||
const key = String(raw)
|
||||
const base = channelMap.value[key] || key
|
||||
const extra = String(row.channel_source_detail ?? '').trim()
|
||||
return extra !== '' ? `${base}(${extra})` : base
|
||||
}
|
||||
|
||||
function sortRows(list: any[]) {
|
||||
return [...list].sort((a, b) => {
|
||||
const da = String(a.appointment_date || '')
|
||||
const db = String(b.appointment_date || '')
|
||||
if (da !== db) return db.localeCompare(da)
|
||||
const ta = String(a.appointment_time || '')
|
||||
const tb = String(b.appointment_time || '')
|
||||
if (ta !== tb) return tb.localeCompare(ta)
|
||||
return Number(b.id || 0) - Number(a.id || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const loadChannels = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
const opts = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
const map: Record<string, string> = {}
|
||||
for (const row of opts) {
|
||||
if (row.value != null) map[String(row.value)] = row.name || String(row.value)
|
||||
}
|
||||
channelMap.value = map
|
||||
} catch {
|
||||
channelMap.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
await loadChannels()
|
||||
const res = await appointmentLists({
|
||||
patient_id: props.diagnosisId,
|
||||
/** 诊单维度拉挂号:后端豁免医生/医助与数据范围收窄 */
|
||||
diag_scope_relax: 1,
|
||||
page_no: 1,
|
||||
page_size: 500
|
||||
})
|
||||
const list = res?.lists || []
|
||||
rows.value = sortRows(list)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="appointment-record-panel">
|
||||
<el-table v-loading="loading" :data="rows" border stripe empty-text="暂无挂号记录">
|
||||
<el-table-column label="ID" prop="id" width="72" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="
|
||||
row.status === 1
|
||||
? 'success'
|
||||
: row.status === 2
|
||||
? 'info'
|
||||
: row.status === 3
|
||||
? 'primary'
|
||||
: 'danger'
|
||||
"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
{{ row.status_desc || '—' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="患者(挂号人)" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="font-medium">{{ row.patient_name || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ row.patient_phone || '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂号医生" prop="doctor_name" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="挂号助理" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.assistant_name || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时间" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span>{{ row.appointment_date || '—' }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ formatTime(row.appointment_time) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时段" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ periodText(row.period) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.appointment_type_desc || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="渠道" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ channelLabel(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="确认诊单" width="92" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.diagnosis_confirmed" type="success" size="small" effect="plain">已确认</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">未确认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开方" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.has_prescription" type="success" size="small" effect="plain">已开方</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未开方</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="165" prop="create_time" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { appointmentLists } from '@/api/doctor'
|
||||
import { getDictData } from '@/api/app'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const channelMap = ref<Record<string, string>>({})
|
||||
|
||||
function formatTime(t: string | undefined) {
|
||||
if (!t) return ''
|
||||
return String(t).length >= 8 ? String(t).slice(0, 5) : t
|
||||
}
|
||||
|
||||
function periodText(p: string | undefined) {
|
||||
if (p === 'morning') return '上午'
|
||||
if (p === 'afternoon') return '下午'
|
||||
if (p === 'all') return '全天'
|
||||
return p || '—'
|
||||
}
|
||||
|
||||
function channelLabel(row: Record<string, any>) {
|
||||
const raw = row.channel_source ?? row.channels ?? ''
|
||||
if (raw === '' || raw === null || raw === undefined) return '—'
|
||||
const key = String(raw)
|
||||
const base = channelMap.value[key] || key
|
||||
const extra = String(row.channel_source_detail ?? '').trim()
|
||||
return extra !== '' ? `${base}(${extra})` : base
|
||||
}
|
||||
|
||||
function sortRows(list: any[]) {
|
||||
return [...list].sort((a, b) => {
|
||||
const da = String(a.appointment_date || '')
|
||||
const db = String(b.appointment_date || '')
|
||||
if (da !== db) return db.localeCompare(da)
|
||||
const ta = String(a.appointment_time || '')
|
||||
const tb = String(b.appointment_time || '')
|
||||
if (ta !== tb) return tb.localeCompare(ta)
|
||||
return Number(b.id || 0) - Number(a.id || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const loadChannels = async () => {
|
||||
try {
|
||||
const data = await getDictData({ type: 'channels' })
|
||||
const opts = (data?.channels || []).filter((row: any) => row.status !== 0)
|
||||
const map: Record<string, string> = {}
|
||||
for (const row of opts) {
|
||||
if (row.value != null) map[String(row.value)] = row.name || String(row.value)
|
||||
}
|
||||
channelMap.value = map
|
||||
} catch {
|
||||
channelMap.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
await loadChannels()
|
||||
const res = await appointmentLists({
|
||||
patient_id: props.diagnosisId,
|
||||
/** 诊单维度拉挂号:后端豁免医生/医助与数据范围收窄 */
|
||||
diag_scope_relax: 1,
|
||||
page_no: 1,
|
||||
page_size: 500
|
||||
})
|
||||
const list = res?.lists || []
|
||||
rows.value = sortRows(list)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,492 +1,492 @@
|
||||
<template>
|
||||
<div class="blood-record-list">
|
||||
<div class="mb-4">
|
||||
<el-button v-if="!readOnly" type="primary" @click="handleAdd">添加记录</el-button>
|
||||
<el-select v-model="trendDays" @change="handleTrendDaysChange" class="ml-2" style="width: 150px">
|
||||
<el-option label="最近7天" :value="7" />
|
||||
<el-option label="最近30天" :value="30" />
|
||||
<el-option label="最近90天" :value="90" />
|
||||
<el-option label="最近一年" :value="365" />
|
||||
<el-option label="自定义" :value="0" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-if="trendDays === 0"
|
||||
v-model="customDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@change="loadTrendData"
|
||||
class="ml-2"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>血糖趋势图</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="chartRef" style="width: 100%; height: 400px"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border style="width: 100%">
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column prop="record_time" label="记录时间" width="100" />
|
||||
<el-table-column label="空腹血糖(mmol/L)" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.fasting_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="餐后2小时血糖(mmol/L)" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.postprandial_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="其他血糖(mmol/L)" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.other_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="血压(mmHg)" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.systolic_pressure && row.diastolic_pressure">
|
||||
{{ row.systolic_pressure }}/{{ row.diastolic_pressure }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="western_medicine" label="西药" show-overflow-tooltip />
|
||||
<el-table-column prop="insulin" label="胰岛素" show-overflow-tooltip />
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="recordForm" label-width="120px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间">
|
||||
<el-time-picker
|
||||
v-model="recordForm.record_time"
|
||||
placeholder="选择时间"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="空腹血糖">
|
||||
<el-input-number
|
||||
v-model="recordForm.fasting_blood_sugar"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
:max="50"
|
||||
placeholder="请输入空腹血糖"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="餐后2小时血糖">
|
||||
<el-input-number
|
||||
v-model="recordForm.postprandial_blood_sugar"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
:max="50"
|
||||
placeholder="请输入餐后2小时血糖"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他血糖">
|
||||
<el-input
|
||||
v-model="recordForm.other_blood_sugar"
|
||||
placeholder="请输入其他血糖"
|
||||
type="number"
|
||||
maxlength="10"
|
||||
@input="handleOtherBloodSugarInput"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="收缩压(高压)">
|
||||
<el-input-number
|
||||
v-model="recordForm.systolic_pressure"
|
||||
:min="0"
|
||||
:max="300"
|
||||
placeholder="请输入收缩压"
|
||||
/>
|
||||
<span class="ml-2">mmHg</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="舒张压(低压)">
|
||||
<el-input-number
|
||||
v-model="recordForm.diastolic_pressure"
|
||||
:min="0"
|
||||
:max="200"
|
||||
placeholder="请输入舒张压"
|
||||
/>
|
||||
<span class="ml-2">mmHg</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="西药">
|
||||
<el-input
|
||||
v-model="recordForm.western_medicine"
|
||||
placeholder="请输入西药"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="胰岛素">
|
||||
<el-input
|
||||
v-model="recordForm.insulin"
|
||||
placeholder="请输入胰岛素"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { bloodRecordAdd, bloodRecordEdit, bloodRecordDelete, getRecordsByPatient, getBloodSugarTrend } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import * as echarts from 'echarts'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
const trendDays = ref(7)
|
||||
const customDateRange = ref<[Date, Date] | null>(null)
|
||||
const trendData = ref({
|
||||
dates: [],
|
||||
fasting: [],
|
||||
postprandial_2h: [],
|
||||
other: []
|
||||
})
|
||||
const chartRef = ref()
|
||||
let chartInstance: any = null
|
||||
let renderRetryCount = 0
|
||||
const MAX_RETRY_COUNT = 5
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
record_time: '',
|
||||
fasting_blood_sugar: 0,
|
||||
postprandial_blood_sugar: 0,
|
||||
other_blood_sugar: 0,
|
||||
systolic_pressure: 0,
|
||||
diastolic_pressure: 0,
|
||||
western_medicine: '',
|
||||
insulin: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
/** 列表展示:空值或 0 / 0.00(含字符串)统一显示为 - */
|
||||
function formatBloodGlucoseCell(v: unknown): string {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
const n = Number(String(v).trim())
|
||||
if (!Number.isFinite(n) || n === 0) return '-'
|
||||
return String(n.toFixed(2).replace(/\.?0+$/, ''))
|
||||
}
|
||||
|
||||
const loadTrendData = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
}
|
||||
|
||||
if (trendDays.value === 0 && customDateRange.value) {
|
||||
params.start_date = Math.floor(customDateRange.value[0].getTime() / 1000)
|
||||
params.end_date = Math.floor(customDateRange.value[1].getTime() / 1000)
|
||||
} else {
|
||||
params.days = trendDays.value
|
||||
}
|
||||
|
||||
const data = await getBloodSugarTrend(params)
|
||||
trendData.value = data
|
||||
renderChart()
|
||||
} catch (error) {
|
||||
console.error('获取趋势图数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrendDaysChange = () => {
|
||||
if (trendDays.value !== 0) {
|
||||
customDateRange.value = null
|
||||
}
|
||||
loadTrendData()
|
||||
}
|
||||
|
||||
const renderChart = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (!chartRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = chartRef.value
|
||||
|
||||
if (container.clientWidth === 0 || container.clientHeight === 0) {
|
||||
if (renderRetryCount < MAX_RETRY_COUNT) {
|
||||
renderRetryCount++
|
||||
setTimeout(() => renderChart(), 100)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 重置重试计数器
|
||||
renderRetryCount = 0
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(container)
|
||||
|
||||
// 准备数据,确保每个日期都有对应的值
|
||||
const prepareData = (dataArray: any[], dates: string[]) => {
|
||||
const dataMap = new Map()
|
||||
dataArray.forEach(item => {
|
||||
dataMap.set(item.date, item.value)
|
||||
})
|
||||
return dates.map(date => dataMap.get(date) || null)
|
||||
}
|
||||
|
||||
const fastingData = prepareData(trendData.value.fasting, trendData.value.dates)
|
||||
const postprandialData = prepareData(trendData.value.postprandial_2h, trendData.value.dates)
|
||||
const otherData = prepareData(trendData.value.other, trendData.value.dates)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['空腹血糖', '餐后2小时血糖', '其他血糖']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trendData.value.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '血糖值 (mmol/L)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '空腹血糖',
|
||||
type: 'line',
|
||||
data: fastingData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
},
|
||||
{
|
||||
name: '餐后2小时血糖',
|
||||
type: 'line',
|
||||
data: postprandialData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
},
|
||||
{
|
||||
name: '其他血糖',
|
||||
type: 'line',
|
||||
data: otherData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
chartInstance.resize()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 获取记录列表
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加记录
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
record_time: '',
|
||||
fasting_blood_sugar: 0,
|
||||
postprandial_blood_sugar: 0,
|
||||
other_blood_sugar: 0,
|
||||
systolic_pressure: 0,
|
||||
diastolic_pressure: 0,
|
||||
western_medicine: '',
|
||||
insulin: '',
|
||||
remark: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = {
|
||||
...row,
|
||||
fasting_blood_sugar: parseFloat(row.fasting_blood_sugar) || 0,
|
||||
postprandial_blood_sugar: parseFloat(row.postprandial_blood_sugar) || 0,
|
||||
other_blood_sugar: parseFloat(row.other_blood_sugar) || 0,
|
||||
systolic_pressure: parseInt(row.systolic_pressure) || 0,
|
||||
diastolic_pressure: parseInt(row.diastolic_pressure) || 0
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理其他血糖输入,只允许数字
|
||||
const handleOtherBloodSugarInput = (value: string) => {
|
||||
recordForm.value.other_blood_sugar = value.replace(/[^\d.]/g, '')
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await bloodRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await bloodRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await bloodRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟加载数据,确保DOM完全渲染
|
||||
setTimeout(() => {
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听props变化
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
})
|
||||
|
||||
// 监听组件可见性变化(Tab切换时)
|
||||
watch(() => props.diagnosisId, (newVal) => {
|
||||
if (newVal) {
|
||||
// 当组件变为可见时,延迟重新渲染图表
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.blood-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="blood-record-list">
|
||||
<div class="mb-4">
|
||||
<el-button v-if="!readOnly" type="primary" @click="handleAdd">添加记录</el-button>
|
||||
<el-select v-model="trendDays" @change="handleTrendDaysChange" class="ml-2" style="width: 150px">
|
||||
<el-option label="最近7天" :value="7" />
|
||||
<el-option label="最近30天" :value="30" />
|
||||
<el-option label="最近90天" :value="90" />
|
||||
<el-option label="最近一年" :value="365" />
|
||||
<el-option label="自定义" :value="0" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-if="trendDays === 0"
|
||||
v-model="customDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@change="loadTrendData"
|
||||
class="ml-2"
|
||||
style="width: 300px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>血糖趋势图</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="chartRef" style="width: 100%; height: 400px"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border style="width: 100%">
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column prop="record_time" label="记录时间" width="100" />
|
||||
<el-table-column label="空腹血糖(mmol/L)" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.fasting_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="餐后2小时血糖(mmol/L)" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.postprandial_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="其他血糖(mmol/L)" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBloodGlucoseCell(row.other_blood_sugar) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="血压(mmHg)" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.systolic_pressure && row.diastolic_pressure">
|
||||
{{ row.systolic_pressure }}/{{ row.diastolic_pressure }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="western_medicine" label="西药" show-overflow-tooltip />
|
||||
<el-table-column prop="insulin" label="胰岛素" show-overflow-tooltip />
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="recordForm" label-width="120px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="记录时间">
|
||||
<el-time-picker
|
||||
v-model="recordForm.record_time"
|
||||
placeholder="选择时间"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="空腹血糖">
|
||||
<el-input-number
|
||||
v-model="recordForm.fasting_blood_sugar"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
:max="50"
|
||||
placeholder="请输入空腹血糖"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="餐后2小时血糖">
|
||||
<el-input-number
|
||||
v-model="recordForm.postprandial_blood_sugar"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
:min="0"
|
||||
:max="50"
|
||||
placeholder="请输入餐后2小时血糖"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他血糖">
|
||||
<el-input
|
||||
v-model="recordForm.other_blood_sugar"
|
||||
placeholder="请输入其他血糖"
|
||||
type="number"
|
||||
maxlength="10"
|
||||
@input="handleOtherBloodSugarInput"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<span class="ml-2">mmol/L</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="收缩压(高压)">
|
||||
<el-input-number
|
||||
v-model="recordForm.systolic_pressure"
|
||||
:min="0"
|
||||
:max="300"
|
||||
placeholder="请输入收缩压"
|
||||
/>
|
||||
<span class="ml-2">mmHg</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="舒张压(低压)">
|
||||
<el-input-number
|
||||
v-model="recordForm.diastolic_pressure"
|
||||
:min="0"
|
||||
:max="200"
|
||||
placeholder="请输入舒张压"
|
||||
/>
|
||||
<span class="ml-2">mmHg</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="西药">
|
||||
<el-input
|
||||
v-model="recordForm.western_medicine"
|
||||
placeholder="请输入西药"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="胰岛素">
|
||||
<el-input
|
||||
v-model="recordForm.insulin"
|
||||
placeholder="请输入胰岛素"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { bloodRecordAdd, bloodRecordEdit, bloodRecordDelete, getRecordsByPatient, getBloodSugarTrend } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
import * as echarts from 'echarts'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
const trendDays = ref(7)
|
||||
const customDateRange = ref<[Date, Date] | null>(null)
|
||||
const trendData = ref({
|
||||
dates: [],
|
||||
fasting: [],
|
||||
postprandial_2h: [],
|
||||
other: []
|
||||
})
|
||||
const chartRef = ref()
|
||||
let chartInstance: any = null
|
||||
let renderRetryCount = 0
|
||||
const MAX_RETRY_COUNT = 5
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
record_time: '',
|
||||
fasting_blood_sugar: 0,
|
||||
postprandial_blood_sugar: 0,
|
||||
other_blood_sugar: 0,
|
||||
systolic_pressure: 0,
|
||||
diastolic_pressure: 0,
|
||||
western_medicine: '',
|
||||
insulin: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
/** 列表展示:空值或 0 / 0.00(含字符串)统一显示为 - */
|
||||
function formatBloodGlucoseCell(v: unknown): string {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
const n = Number(String(v).trim())
|
||||
if (!Number.isFinite(n) || n === 0) return '-'
|
||||
return String(n.toFixed(2).replace(/\.?0+$/, ''))
|
||||
}
|
||||
|
||||
const loadTrendData = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
}
|
||||
|
||||
if (trendDays.value === 0 && customDateRange.value) {
|
||||
params.start_date = Math.floor(customDateRange.value[0].getTime() / 1000)
|
||||
params.end_date = Math.floor(customDateRange.value[1].getTime() / 1000)
|
||||
} else {
|
||||
params.days = trendDays.value
|
||||
}
|
||||
|
||||
const data = await getBloodSugarTrend(params)
|
||||
trendData.value = data
|
||||
renderChart()
|
||||
} catch (error) {
|
||||
console.error('获取趋势图数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrendDaysChange = () => {
|
||||
if (trendDays.value !== 0) {
|
||||
customDateRange.value = null
|
||||
}
|
||||
loadTrendData()
|
||||
}
|
||||
|
||||
const renderChart = () => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if (!chartRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = chartRef.value
|
||||
|
||||
if (container.clientWidth === 0 || container.clientHeight === 0) {
|
||||
if (renderRetryCount < MAX_RETRY_COUNT) {
|
||||
renderRetryCount++
|
||||
setTimeout(() => renderChart(), 100)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 重置重试计数器
|
||||
renderRetryCount = 0
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(container)
|
||||
|
||||
// 准备数据,确保每个日期都有对应的值
|
||||
const prepareData = (dataArray: any[], dates: string[]) => {
|
||||
const dataMap = new Map()
|
||||
dataArray.forEach(item => {
|
||||
dataMap.set(item.date, item.value)
|
||||
})
|
||||
return dates.map(date => dataMap.get(date) || null)
|
||||
}
|
||||
|
||||
const fastingData = prepareData(trendData.value.fasting, trendData.value.dates)
|
||||
const postprandialData = prepareData(trendData.value.postprandial_2h, trendData.value.dates)
|
||||
const otherData = prepareData(trendData.value.other, trendData.value.dates)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['空腹血糖', '餐后2小时血糖', '其他血糖']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trendData.value.dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '血糖值 (mmol/L)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '空腹血糖',
|
||||
type: 'line',
|
||||
data: fastingData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
},
|
||||
{
|
||||
name: '餐后2小时血糖',
|
||||
type: 'line',
|
||||
data: postprandialData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
},
|
||||
{
|
||||
name: '其他血糖',
|
||||
type: 'line',
|
||||
data: otherData,
|
||||
smooth: true,
|
||||
connectNulls: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
chartInstance.resize()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 获取记录列表
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加记录
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
record_time: '',
|
||||
fasting_blood_sugar: 0,
|
||||
postprandial_blood_sugar: 0,
|
||||
other_blood_sugar: 0,
|
||||
systolic_pressure: 0,
|
||||
diastolic_pressure: 0,
|
||||
western_medicine: '',
|
||||
insulin: '',
|
||||
remark: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑记录
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = {
|
||||
...row,
|
||||
fasting_blood_sugar: parseFloat(row.fasting_blood_sugar) || 0,
|
||||
postprandial_blood_sugar: parseFloat(row.postprandial_blood_sugar) || 0,
|
||||
other_blood_sugar: parseFloat(row.other_blood_sugar) || 0,
|
||||
systolic_pressure: parseInt(row.systolic_pressure) || 0,
|
||||
diastolic_pressure: parseInt(row.diastolic_pressure) || 0
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理其他血糖输入,只允许数字
|
||||
const handleOtherBloodSugarInput = (value: string) => {
|
||||
recordForm.value.other_blood_sugar = value.replace(/[^\d.]/g, '')
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await bloodRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await bloodRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await bloodRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟加载数据,确保DOM完全渲染
|
||||
setTimeout(() => {
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听props变化
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
loadTrendData()
|
||||
})
|
||||
|
||||
// 监听组件可见性变化(Tab切换时)
|
||||
watch(() => props.diagnosisId, (newVal) => {
|
||||
if (newVal) {
|
||||
// 当组件变为可见时,延迟重新渲染图表
|
||||
setTimeout(() => {
|
||||
renderChart()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.blood-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,214 +1,214 @@
|
||||
<template>
|
||||
<div class="call-record-panel">
|
||||
<div v-if="!readOnly" class="call-record-toolbar">
|
||||
<upload
|
||||
ref="toolbarUploadRef"
|
||||
type="video"
|
||||
direct
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
:show-progress="true"
|
||||
@success="handleToolbarUploadSuccess"
|
||||
>
|
||||
<el-button type="primary">上传视频</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<template #empty>
|
||||
<div class="call-record-empty">
|
||||
<div class="call-record-empty__title">暂无通话记录</div>
|
||||
<div class="call-record-empty__desc">
|
||||
{{
|
||||
readOnly
|
||||
? '暂无录制回放数据。'
|
||||
: '现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column label="录制回放" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<RecordingPlaybackBlock :record-id="row.id" :urls="row.recording_urls_list" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" width="170" prop="start_time_text" />
|
||||
<el-table-column label="结束时间" width="170" prop="end_time_text" />
|
||||
<el-table-column label="通话类型" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.call_type === 1 ? '语音' : '视频' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="房间号" width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长" width="110" prop="duration_text" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ statusText(row.status) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录制" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.recording_status_text || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!readOnly" label="上传回放" width="180">
|
||||
<template #default="{ row }">
|
||||
<upload
|
||||
type="video"
|
||||
direct
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
:show-progress="true"
|
||||
@success="(response: any) => handleRowUploadSuccess(row, response)"
|
||||
>
|
||||
<el-button type="primary" plain size="small">上传视频</el-button>
|
||||
</upload>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RecordingPlaybackBlock from './RecordingPlaybackBlock.vue'
|
||||
import Upload from '@/components/upload/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { attachLocalCallRecording, createManualCallRecord, getCallRecords } from '@/api/tcm'
|
||||
import { ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
type CallRecordRow = {
|
||||
id: number
|
||||
call_type: number
|
||||
room_id?: string
|
||||
status: number
|
||||
recording_status_text?: string
|
||||
recording_urls_list?: string[]
|
||||
start_time_text?: string
|
||||
end_time_text?: string
|
||||
duration_text?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
diagnosisId: number
|
||||
/** 只读场景(如诊单只读详情):不展示任何上传入口 */
|
||||
readOnly?: boolean
|
||||
}>(),
|
||||
{ readOnly: false }
|
||||
)
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<CallRecordRow[]>([])
|
||||
const toolbarUploadRef = useTemplateRef<InstanceType<typeof Upload>>('toolbarUploadRef')
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = (await getCallRecords({ diagnosis_id: props.diagnosisId })) || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
void load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
|
||||
function statusText(status: number) {
|
||||
const m: Record<number, string> = {
|
||||
1: '进行中',
|
||||
2: '已结束',
|
||||
3: '未接听',
|
||||
4: '已取消'
|
||||
}
|
||||
return m[status] ?? '—'
|
||||
}
|
||||
|
||||
async function handleToolbarUploadSuccess(response: any) {
|
||||
const fileUrl = normalizeUploadUrl(response)
|
||||
if (!fileUrl) {
|
||||
feedback.msgError('上传成功但未返回视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await createManualCallRecord({ diagnosis_id: props.diagnosisId })
|
||||
await attachLocalCallRecording({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
call_record_id: Number(record?.id || 0),
|
||||
file_url: fileUrl
|
||||
} as any)
|
||||
feedback.msgSuccess('视频回放上传成功')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
feedback.msgError(error?.message || '写入回放失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRowUploadSuccess(row: CallRecordRow, response: any) {
|
||||
const fileUrl = normalizeUploadUrl(response)
|
||||
if (!fileUrl) {
|
||||
feedback.msgError('上传成功但未返回视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await attachLocalCallRecording({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
call_record_id: Number(row.id || 0),
|
||||
file_url: fileUrl
|
||||
} as any)
|
||||
feedback.msgSuccess('视频回放上传成功')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
feedback.msgError(error?.message || '写入回放失败')
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUploadUrl(response: any): string {
|
||||
return String(response?.data?.uri || response?.data?.url || '').trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.call-record-panel {
|
||||
.call-record-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.call-record-empty {
|
||||
padding: 28px 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.call-record-empty__title {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.call-record-empty__desc {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="call-record-panel">
|
||||
<div v-if="!readOnly" class="call-record-toolbar">
|
||||
<upload
|
||||
ref="toolbarUploadRef"
|
||||
type="video"
|
||||
direct
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
:show-progress="true"
|
||||
@success="handleToolbarUploadSuccess"
|
||||
>
|
||||
<el-button type="primary">上传视频</el-button>
|
||||
</upload>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border stripe>
|
||||
<template #empty>
|
||||
<div class="call-record-empty">
|
||||
<div class="call-record-empty__title">暂无通话记录</div>
|
||||
<div class="call-record-empty__desc">
|
||||
{{
|
||||
readOnly
|
||||
? '暂无录制回放数据。'
|
||||
: '现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table-column label="录制回放" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<RecordingPlaybackBlock :record-id="row.id" :urls="row.recording_urls_list" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" width="170" prop="start_time_text" />
|
||||
<el-table-column label="结束时间" width="170" prop="end_time_text" />
|
||||
<el-table-column label="通话类型" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.call_type === 1 ? '语音' : '视频' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="房间号" width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长" width="110" prop="duration_text" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ statusText(row.status) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录制" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.recording_status_text || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!readOnly" label="上传回放" width="180">
|
||||
<template #default="{ row }">
|
||||
<upload
|
||||
type="video"
|
||||
direct
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
:show-progress="true"
|
||||
@success="(response: any) => handleRowUploadSuccess(row, response)"
|
||||
>
|
||||
<el-button type="primary" plain size="small">上传视频</el-button>
|
||||
</upload>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RecordingPlaybackBlock from './RecordingPlaybackBlock.vue'
|
||||
import Upload from '@/components/upload/index.vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
import { attachLocalCallRecording, createManualCallRecord, getCallRecords } from '@/api/tcm'
|
||||
import { ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
type CallRecordRow = {
|
||||
id: number
|
||||
call_type: number
|
||||
room_id?: string
|
||||
status: number
|
||||
recording_status_text?: string
|
||||
recording_urls_list?: string[]
|
||||
start_time_text?: string
|
||||
end_time_text?: string
|
||||
duration_text?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
diagnosisId: number
|
||||
/** 只读场景(如诊单只读详情):不展示任何上传入口 */
|
||||
readOnly?: boolean
|
||||
}>(),
|
||||
{ readOnly: false }
|
||||
)
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<CallRecordRow[]>([])
|
||||
const toolbarUploadRef = useTemplateRef<InstanceType<typeof Upload>>('toolbarUploadRef')
|
||||
|
||||
const load = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
rows.value = (await getCallRecords({ diagnosis_id: props.diagnosisId })) || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
void load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: load })
|
||||
|
||||
function statusText(status: number) {
|
||||
const m: Record<number, string> = {
|
||||
1: '进行中',
|
||||
2: '已结束',
|
||||
3: '未接听',
|
||||
4: '已取消'
|
||||
}
|
||||
return m[status] ?? '—'
|
||||
}
|
||||
|
||||
async function handleToolbarUploadSuccess(response: any) {
|
||||
const fileUrl = normalizeUploadUrl(response)
|
||||
if (!fileUrl) {
|
||||
feedback.msgError('上传成功但未返回视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const record = await createManualCallRecord({ diagnosis_id: props.diagnosisId })
|
||||
await attachLocalCallRecording({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
call_record_id: Number(record?.id || 0),
|
||||
file_url: fileUrl
|
||||
} as any)
|
||||
feedback.msgSuccess('视频回放上传成功')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
feedback.msgError(error?.message || '写入回放失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRowUploadSuccess(row: CallRecordRow, response: any) {
|
||||
const fileUrl = normalizeUploadUrl(response)
|
||||
if (!fileUrl) {
|
||||
feedback.msgError('上传成功但未返回视频地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await attachLocalCallRecording({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
call_record_id: Number(row.id || 0),
|
||||
file_url: fileUrl
|
||||
} as any)
|
||||
feedback.msgSuccess('视频回放上传成功')
|
||||
await load()
|
||||
} catch (error: any) {
|
||||
feedback.msgError(error?.message || '写入回放失败')
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUploadUrl(response: any): string {
|
||||
return String(response?.data?.uri || response?.data?.url || '').trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.call-record-panel {
|
||||
.call-record-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.call-record-empty {
|
||||
padding: 28px 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.call-record-empty__title {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.call-record-empty__desc {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,107 +1,107 @@
|
||||
<template>
|
||||
<div class="case-record-list">
|
||||
<div v-if="!readOnly" class="mb-3 flex justify-end">
|
||||
<el-button type="primary" size="small" @click="handleOpenPrescription">开方</el-button>
|
||||
</div>
|
||||
<el-table :data="caseList" border v-loading="loading">
|
||||
<el-table-column prop="prescription_date" label="就诊日期" width="120" />
|
||||
<el-table-column prop="visit_no" label="门诊号" width="120" />
|
||||
<el-table-column prop="clinical_diagnosis" label="临床诊断" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="处方摘要" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.herbs && row.herbs.length">
|
||||
{{ row.herbs.slice(0, 3).map((h: any) => `${h.name}${h.dosage}克`).join('、') }}{{ row.herbs.length > 3 ? '...' : '' }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doctor_name" label="医师" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.void_status === 1">
|
||||
<el-tag type="danger" size="small">已作废</el-tag>
|
||||
<div class="void-detail text-xs text-gray-500 mt-1">
|
||||
{{ row.void_by_name || '—' }} {{ formatVoidTime(row.void_time) }}
|
||||
</div>
|
||||
</template>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleView(row)">
|
||||
查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && caseList.length === 0" description="暂无病历记录,开方后会自动显示" class="mt-4" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { prescriptionListByDiagnosis } from '@/api/tcm'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 只读:隐藏开方(与诊单 openViewOnly 一致) */
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['view', 'openPrescription'])
|
||||
|
||||
const caseList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const getCases = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await prescriptionListByDiagnosis({ diagnosis_id: props.diagnosisId })
|
||||
caseList.value = Array.isArray(data) ? data : []
|
||||
} catch (error) {
|
||||
console.error('获取病历记录失败:', error)
|
||||
caseList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatVoidTime = (ts: number | null | undefined) => {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
emit('view', row)
|
||||
}
|
||||
|
||||
const handleOpenPrescription = () => {
|
||||
emit('openPrescription')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCases()
|
||||
})
|
||||
|
||||
watch(() => props.diagnosisId, () => {
|
||||
getCases()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
refresh: getCases
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.case-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="case-record-list">
|
||||
<div v-if="!readOnly" class="mb-3 flex justify-end">
|
||||
<el-button type="primary" size="small" @click="handleOpenPrescription">开方</el-button>
|
||||
</div>
|
||||
<el-table :data="caseList" border v-loading="loading">
|
||||
<el-table-column prop="prescription_date" label="就诊日期" width="120" />
|
||||
<el-table-column prop="visit_no" label="门诊号" width="120" />
|
||||
<el-table-column prop="clinical_diagnosis" label="临床诊断" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="处方摘要" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.herbs && row.herbs.length">
|
||||
{{ row.herbs.slice(0, 3).map((h: any) => `${h.name}${h.dosage}克`).join('、') }}{{ row.herbs.length > 3 ? '...' : '' }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="doctor_name" label="医师" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.void_status === 1">
|
||||
<el-tag type="danger" size="small">已作废</el-tag>
|
||||
<div class="void-detail text-xs text-gray-500 mt-1">
|
||||
{{ row.void_by_name || '—' }} {{ formatVoidTime(row.void_time) }}
|
||||
</div>
|
||||
</template>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleView(row)">
|
||||
查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && caseList.length === 0" description="暂无病历记录,开方后会自动显示" class="mt-4" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { prescriptionListByDiagnosis } from '@/api/tcm'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 只读:隐藏开方(与诊单 openViewOnly 一致) */
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['view', 'openPrescription'])
|
||||
|
||||
const caseList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const getCases = async () => {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await prescriptionListByDiagnosis({ diagnosis_id: props.diagnosisId })
|
||||
caseList.value = Array.isArray(data) ? data : []
|
||||
} catch (error) {
|
||||
console.error('获取病历记录失败:', error)
|
||||
caseList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatVoidTime = (ts: number | null | undefined) => {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
emit('view', row)
|
||||
}
|
||||
|
||||
const handleOpenPrescription = () => {
|
||||
emit('openPrescription')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCases()
|
||||
})
|
||||
|
||||
watch(() => props.diagnosisId, () => {
|
||||
getCases()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
refresh: getCases
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.case-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,240 +1,240 @@
|
||||
<template>
|
||||
<div class="diet-record-list">
|
||||
<div v-if="!readOnly" class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border>
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column label="早餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.breakfast_foods">{{ row.breakfast_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="午餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.lunch_foods">{{ row.lunch_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="晚餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.dinner_foods">{{ row.dinner_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
||||
<el-form :model="recordForm" label-width="100px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">早餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.breakfast_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入早餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.breakfast_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">午餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.lunch_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入午餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.lunch_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">晚餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.dinner_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入晚餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.dinner_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dietRecordAdd, dietRecordEdit, dietRecordDelete, getDietRecordsByPatient } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
breakfast_foods: '',
|
||||
breakfast_images: [],
|
||||
lunch_foods: '',
|
||||
lunch_images: [],
|
||||
dinner_foods: '',
|
||||
dinner_images: [],
|
||||
note: ''
|
||||
})
|
||||
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getDietRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
breakfast_foods: '',
|
||||
breakfast_images: [],
|
||||
lunch_foods: '',
|
||||
lunch_images: [],
|
||||
dinner_foods: '',
|
||||
dinner_images: [],
|
||||
note: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await dietRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await dietRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await dietRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRecords()
|
||||
})
|
||||
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diet-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="diet-record-list">
|
||||
<div v-if="!readOnly" class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border>
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column label="早餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.breakfast_foods">{{ row.breakfast_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="午餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.lunch_foods">{{ row.lunch_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="晚餐" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.dinner_foods">{{ row.dinner_foods }}</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
|
||||
<el-form :model="recordForm" label-width="100px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">早餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.breakfast_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入早餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.breakfast_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">午餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.lunch_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入午餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.lunch_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">晚餐</el-divider>
|
||||
<el-form-item label="食物描述">
|
||||
<el-input
|
||||
v-model="recordForm.dinner_foods"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入晚餐食物"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片">
|
||||
<material-picker
|
||||
v-model="recordForm.dinner_images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dietRecordAdd, dietRecordEdit, dietRecordDelete, getDietRecordsByPatient } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
breakfast_foods: '',
|
||||
breakfast_images: [],
|
||||
lunch_foods: '',
|
||||
lunch_images: [],
|
||||
dinner_foods: '',
|
||||
dinner_images: [],
|
||||
note: ''
|
||||
})
|
||||
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getDietRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
breakfast_foods: '',
|
||||
breakfast_images: [],
|
||||
lunch_foods: '',
|
||||
lunch_images: [],
|
||||
dinner_foods: '',
|
||||
dinner_images: [],
|
||||
note: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await dietRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await dietRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await dietRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRecords()
|
||||
})
|
||||
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diet-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,231 +1,231 @@
|
||||
<template>
|
||||
<div class="exercise-record-list">
|
||||
<div v-if="!readOnly" class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border style="width: 100%">
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column prop="exercise_type" label="运动类型" width="150" />
|
||||
<el-table-column prop="duration" label="运动时长" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration }} 分钟
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运动强度" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getIntensityText(row.intensity) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="recordForm" label-width="120px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动类型" required>
|
||||
<el-select v-model="recordForm.exercise_type" placeholder="请选择运动类型" class="w-full">
|
||||
<el-option label="散步" :value="'散步'" />
|
||||
<el-option label="慢跑" :value="'慢跑'" />
|
||||
<el-option label="游泳" :value="'游泳'" />
|
||||
<el-option label="瑜伽" :value="'瑜伽'" />
|
||||
<el-option label="骑自行车" :value="'骑自行车'" />
|
||||
<el-option label="打太极拳" :value="'打太极拳'" />
|
||||
<el-option label="其他" :value="'其他'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动时长(分钟)" required>
|
||||
<el-input-number
|
||||
v-model="recordForm.duration"
|
||||
:min="0"
|
||||
:max="300"
|
||||
placeholder="请输入运动时长"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动强度" required>
|
||||
<el-select v-model="recordForm.intensity" placeholder="请选择运动强度" class="w-full">
|
||||
<el-option label="低强度" :value="1" />
|
||||
<el-option label="中强度" :value="2" />
|
||||
<el-option label="高强度" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动图片">
|
||||
<material-picker
|
||||
v-model="recordForm.images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { exerciseRecordAdd, exerciseRecordEdit, exerciseRecordDelete, getExerciseRecordsByPatient } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
exercise_type: '',
|
||||
duration: 0,
|
||||
intensity: 1,
|
||||
images: [],
|
||||
note: ''
|
||||
})
|
||||
|
||||
const intensityOptions = [
|
||||
{ label: '低强度', value: 1 },
|
||||
{ label: '中强度', value: 2 },
|
||||
{ label: '高强度', value: 3 }
|
||||
]
|
||||
|
||||
const getIntensityText = (intensity: number) => {
|
||||
const option = intensityOptions.find(opt => opt.value === intensity)
|
||||
return option ? option.label : '未知'
|
||||
}
|
||||
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getExerciseRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
exercise_type: '',
|
||||
duration: 0,
|
||||
intensity: 1,
|
||||
images: [],
|
||||
note: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await exerciseRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!recordForm.value.exercise_type) {
|
||||
feedback.msgWarning('请选择运动类型')
|
||||
return
|
||||
}
|
||||
|
||||
if (!recordForm.value.duration) {
|
||||
feedback.msgWarning('请输入运动时长')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await exerciseRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await exerciseRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRecords()
|
||||
})
|
||||
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exercise-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="exercise-record-list">
|
||||
<div v-if="!readOnly" class="mb-4">
|
||||
<el-button type="primary" @click="handleAdd">添加记录</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="recordList" border style="width: 100%">
|
||||
<el-table-column prop="record_date" label="记录日期" width="120" />
|
||||
<el-table-column prop="exercise_type" label="运动类型" width="150" />
|
||||
<el-table-column prop="duration" label="运动时长" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration }} 分钟
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运动强度" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getIntensityText(row.intensity) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" show-overflow-tooltip />
|
||||
<el-table-column v-if="!readOnly" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
|
||||
<el-form :model="recordForm" label-width="120px">
|
||||
<el-form-item label="记录日期" required>
|
||||
<el-date-picker
|
||||
v-model="recordForm.record_date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动类型" required>
|
||||
<el-select v-model="recordForm.exercise_type" placeholder="请选择运动类型" class="w-full">
|
||||
<el-option label="散步" :value="'散步'" />
|
||||
<el-option label="慢跑" :value="'慢跑'" />
|
||||
<el-option label="游泳" :value="'游泳'" />
|
||||
<el-option label="瑜伽" :value="'瑜伽'" />
|
||||
<el-option label="骑自行车" :value="'骑自行车'" />
|
||||
<el-option label="打太极拳" :value="'打太极拳'" />
|
||||
<el-option label="其他" :value="'其他'" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动时长(分钟)" required>
|
||||
<el-input-number
|
||||
v-model="recordForm.duration"
|
||||
:min="0"
|
||||
:max="300"
|
||||
placeholder="请输入运动时长"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动强度" required>
|
||||
<el-select v-model="recordForm.intensity" placeholder="请选择运动强度" class="w-full">
|
||||
<el-option label="低强度" :value="1" />
|
||||
<el-option label="中强度" :value="2" />
|
||||
<el-option label="高强度" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运动图片">
|
||||
<material-picker
|
||||
v-model="recordForm.images"
|
||||
:limit="3"
|
||||
type="image"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="recordForm.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { exerciseRecordAdd, exerciseRecordEdit, exerciseRecordDelete, getExerciseRecordsByPatient } from '@/api/tcm'
|
||||
import feedback from '@/utils/feedback'
|
||||
|
||||
const props = defineProps({
|
||||
diagnosisId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
patientId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const recordList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const dialogTitle = computed(() => recordForm.value.id ? '编辑记录' : '添加记录')
|
||||
|
||||
const recordForm = ref({
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
exercise_type: '',
|
||||
duration: 0,
|
||||
intensity: 1,
|
||||
images: [],
|
||||
note: ''
|
||||
})
|
||||
|
||||
const intensityOptions = [
|
||||
{ label: '低强度', value: 1 },
|
||||
{ label: '中强度', value: 2 },
|
||||
{ label: '高强度', value: 3 }
|
||||
]
|
||||
|
||||
const getIntensityText = (intensity: number) => {
|
||||
const option = intensityOptions.find(opt => opt.value === intensity)
|
||||
return option ? option.label : '未知'
|
||||
}
|
||||
|
||||
const getRecords = async () => {
|
||||
if (!props.diagnosisId && !props.patientId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getExerciseRecordsByPatient({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId
|
||||
})
|
||||
recordList.value = data
|
||||
} catch (error) {
|
||||
console.error('获取记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
recordForm.value = {
|
||||
id: '',
|
||||
diagnosis_id: props.diagnosisId,
|
||||
patient_id: props.patientId,
|
||||
record_date: '',
|
||||
exercise_type: '',
|
||||
duration: 0,
|
||||
intensity: 1,
|
||||
images: [],
|
||||
note: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
recordForm.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
await feedback.confirm('确定要删除这条记录吗?')
|
||||
try {
|
||||
await exerciseRecordDelete({ id: row.id })
|
||||
feedback.msgSuccess('删除成功')
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!recordForm.value.record_date) {
|
||||
feedback.msgWarning('请选择记录日期')
|
||||
return
|
||||
}
|
||||
|
||||
if (!recordForm.value.exercise_type) {
|
||||
feedback.msgWarning('请选择运动类型')
|
||||
return
|
||||
}
|
||||
|
||||
if (!recordForm.value.duration) {
|
||||
feedback.msgWarning('请输入运动时长')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (recordForm.value.id) {
|
||||
await exerciseRecordEdit(recordForm.value)
|
||||
feedback.msgSuccess('编辑成功')
|
||||
} else {
|
||||
await exerciseRecordAdd(recordForm.value)
|
||||
feedback.msgSuccess('添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getRecords()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRecords()
|
||||
})
|
||||
|
||||
watch(() => [props.diagnosisId, props.patientId], () => {
|
||||
getRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exercise-record-list {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,293 +1,293 @@
|
||||
<template>
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示单聊记录:已合并患者 与
|
||||
<strong>所有医生 / 医助账号</strong>分别产生的会话,按时间排序。
|
||||
数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar mb-3">
|
||||
<el-button type="primary" link :loading="syncing" @click="triggerSync">
|
||||
<el-icon class="mr-1"><Promotion /></el-icon>
|
||||
同步最新(后台异步)
|
||||
</el-button>
|
||||
<el-button type="primary" link :loading="loading" @click="reloadArchived">
|
||||
<el-icon class="mr-1"><Refresh /></el-icon>
|
||||
重新加载已归档
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="chat-wrap">
|
||||
<template v-if="!loading && rows.length">
|
||||
<div class="chat-list">
|
||||
<div
|
||||
v-for="item in rows"
|
||||
:key="item.raw.msg_id"
|
||||
class="chat-row"
|
||||
:class="item.raw.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
||||
>
|
||||
<div class="meta">
|
||||
<span class="name">{{ senderLabel(item.raw) }}</span>
|
||||
<span class="time">{{ formatTime(item.raw.time) }}</span>
|
||||
<el-tag v-if="item.tag" size="small" type="info" class="ml-2">
|
||||
{{ item.tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="bubble">
|
||||
<template v-if="item.raw.msg_type === 'image' && item.raw.image_url">
|
||||
<el-image
|
||||
:src="item.raw.image_url"
|
||||
:preview-src-list="[item.raw.image_url]"
|
||||
fit="contain"
|
||||
class="chat-img"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="(item.raw.msg_type === 'file' || item.raw.msg_type === 'sound' || item.raw.msg_type === 'video') && item.raw.file_url">
|
||||
<el-link :href="item.raw.file_url" target="_blank" type="primary">
|
||||
{{ item.raw.file_name || '打开文件' }}
|
||||
</el-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="item.friendly" class="friendly-text">
|
||||
<div class="friendly-main">{{ item.friendly.main }}</div>
|
||||
<div v-if="item.friendly.sub" class="friendly-sub">{{ item.friendly.sub }}</div>
|
||||
</div>
|
||||
<div v-else-if="item.raw.msg_type === 'text' && item.raw.text" class="text-content">
|
||||
{{ item.raw.text }}
|
||||
</div>
|
||||
<div v-else-if="item.raw.text" class="text-content">{{ item.raw.text }}</div>
|
||||
<span v-else class="muted">(无法展示该消息类型)</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="暂无 IM 聊天记录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh, Promotion } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getImChatMessages, triggerImChatSync } from '@/api/tcm'
|
||||
import type { FriendlyParse } from '@/utils/im-business-message-parse'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const rawRows = shallowRef<any[]>([])
|
||||
const patientImId = ref('')
|
||||
const patientName = ref('')
|
||||
|
||||
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
||||
|
||||
interface EnrichedRow {
|
||||
raw: any
|
||||
friendly: FriendlyParse | null
|
||||
tag: string
|
||||
}
|
||||
|
||||
const MSG_TYPE_LABELS: Record<string, string> = {
|
||||
text: '',
|
||||
image: '图片',
|
||||
file: '文件',
|
||||
sound: '语音',
|
||||
video: '视频',
|
||||
location: '位置',
|
||||
custom: '自定义',
|
||||
face: '表情',
|
||||
other: ''
|
||||
}
|
||||
|
||||
const rows = computed<EnrichedRow[]>(() =>
|
||||
rawRows.value.map((row) => {
|
||||
const fr = parseImFriendly(row)
|
||||
let tag = ''
|
||||
if (fr?.tag) {
|
||||
tag = fr.tag
|
||||
} else {
|
||||
tag = MSG_TYPE_LABELS[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||
}
|
||||
return { raw: row, friendly: fr, tag }
|
||||
})
|
||||
)
|
||||
|
||||
function formatTime(ts: number | undefined) {
|
||||
if (ts == null || !ts) return '—'
|
||||
const sec = ts > 1e12 ? Math.floor(ts / 1000) : ts
|
||||
return dayjs.unix(sec).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
function parseImFriendly(row: any): FriendlyParse | null {
|
||||
const raw = (row.text || '').trim()
|
||||
if (!raw) return null
|
||||
const looksLikeBizJson =
|
||||
raw.startsWith('{') && (/\bbusinessID\b/.test(raw) || /\bcmd\b/.test(raw))
|
||||
if (row.msg_type === 'custom' || looksLikeBizJson) {
|
||||
return parseImBusinessPayload(raw)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function senderLabel(row: any) {
|
||||
if (row.is_from_doctor) {
|
||||
return row.from_staff_name ? `医生(${row.from_staff_name})` : '医生/员工'
|
||||
}
|
||||
return patientName.value ? `患者(${patientName.value})` : '患者'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = (await getImChatMessages({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
only_archived: 1
|
||||
})) as {
|
||||
lists?: any[]
|
||||
patient_im_id?: string
|
||||
patient_name?: string
|
||||
}
|
||||
rawRows.value = res?.lists || []
|
||||
patientImId.value = res?.patient_im_id || ''
|
||||
patientName.value = res?.patient_name || ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rawRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reloadArchived() {
|
||||
load()
|
||||
}
|
||||
|
||||
async function triggerSync() {
|
||||
if (!props.diagnosisId) return
|
||||
syncing.value = true
|
||||
try {
|
||||
await triggerImChatSync({ diagnosis_id: props.diagnosisId })
|
||||
ElMessage.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('发起同步失败')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: reloadArchived })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.im-chat-record-panel {
|
||||
min-height: 200px;
|
||||
}
|
||||
.panel-tip {
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
font-size: 13px;
|
||||
code {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chat-wrap {
|
||||
min-height: 120px;
|
||||
}
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: min(60vh, 520px);
|
||||
overflow-y: auto;
|
||||
padding: 4px 8px 12px;
|
||||
}
|
||||
.chat-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 88%;
|
||||
&.from-patient {
|
||||
align-self: flex-start;
|
||||
.bubble {
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
&.from-doctor {
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
.meta {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.bubble {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
.name {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
.bubble {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-img {
|
||||
max-width: 240px;
|
||||
max-height: 200px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.friendly-text {
|
||||
.friendly-main {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.friendly-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="im-chat-record-panel">
|
||||
<el-alert type="info" show-icon :closable="false" class="mb-4" title="说明">
|
||||
<p class="panel-tip">
|
||||
展示单聊记录:已合并患者 与
|
||||
<strong>所有医生 / 医助账号</strong>分别产生的会话,按时间排序。
|
||||
数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。
|
||||
</p>
|
||||
</el-alert>
|
||||
|
||||
<div class="toolbar mb-3">
|
||||
<el-button type="primary" link :loading="syncing" @click="triggerSync">
|
||||
<el-icon class="mr-1"><Promotion /></el-icon>
|
||||
同步最新(后台异步)
|
||||
</el-button>
|
||||
<el-button type="primary" link :loading="loading" @click="reloadArchived">
|
||||
<el-icon class="mr-1"><Refresh /></el-icon>
|
||||
重新加载已归档
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="chat-wrap">
|
||||
<template v-if="!loading && rows.length">
|
||||
<div class="chat-list">
|
||||
<div
|
||||
v-for="item in rows"
|
||||
:key="item.raw.msg_id"
|
||||
class="chat-row"
|
||||
:class="item.raw.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
||||
>
|
||||
<div class="meta">
|
||||
<span class="name">{{ senderLabel(item.raw) }}</span>
|
||||
<span class="time">{{ formatTime(item.raw.time) }}</span>
|
||||
<el-tag v-if="item.tag" size="small" type="info" class="ml-2">
|
||||
{{ item.tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="bubble">
|
||||
<template v-if="item.raw.msg_type === 'image' && item.raw.image_url">
|
||||
<el-image
|
||||
:src="item.raw.image_url"
|
||||
:preview-src-list="[item.raw.image_url]"
|
||||
fit="contain"
|
||||
class="chat-img"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="(item.raw.msg_type === 'file' || item.raw.msg_type === 'sound' || item.raw.msg_type === 'video') && item.raw.file_url">
|
||||
<el-link :href="item.raw.file_url" target="_blank" type="primary">
|
||||
{{ item.raw.file_name || '打开文件' }}
|
||||
</el-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="item.friendly" class="friendly-text">
|
||||
<div class="friendly-main">{{ item.friendly.main }}</div>
|
||||
<div v-if="item.friendly.sub" class="friendly-sub">{{ item.friendly.sub }}</div>
|
||||
</div>
|
||||
<div v-else-if="item.raw.msg_type === 'text' && item.raw.text" class="text-content">
|
||||
{{ item.raw.text }}
|
||||
</div>
|
||||
<div v-else-if="item.raw.text" class="text-content">{{ item.raw.text }}</div>
|
||||
<span v-else class="muted">(无法展示该消息类型)</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else-if="!loading" description="暂无 IM 聊天记录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh, Promotion } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getImChatMessages, triggerImChatSync } from '@/api/tcm'
|
||||
import type { FriendlyParse } from '@/utils/im-business-message-parse'
|
||||
import { parseImBusinessPayload } from '@/utils/im-business-message-parse'
|
||||
|
||||
const props = defineProps<{
|
||||
diagnosisId: number
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const rawRows = shallowRef<any[]>([])
|
||||
const patientImId = ref('')
|
||||
const patientName = ref('')
|
||||
|
||||
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
||||
|
||||
interface EnrichedRow {
|
||||
raw: any
|
||||
friendly: FriendlyParse | null
|
||||
tag: string
|
||||
}
|
||||
|
||||
const MSG_TYPE_LABELS: Record<string, string> = {
|
||||
text: '',
|
||||
image: '图片',
|
||||
file: '文件',
|
||||
sound: '语音',
|
||||
video: '视频',
|
||||
location: '位置',
|
||||
custom: '自定义',
|
||||
face: '表情',
|
||||
other: ''
|
||||
}
|
||||
|
||||
const rows = computed<EnrichedRow[]>(() =>
|
||||
rawRows.value.map((row) => {
|
||||
const fr = parseImFriendly(row)
|
||||
let tag = ''
|
||||
if (fr?.tag) {
|
||||
tag = fr.tag
|
||||
} else {
|
||||
tag = MSG_TYPE_LABELS[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||
}
|
||||
return { raw: row, friendly: fr, tag }
|
||||
})
|
||||
)
|
||||
|
||||
function formatTime(ts: number | undefined) {
|
||||
if (ts == null || !ts) return '—'
|
||||
const sec = ts > 1e12 ? Math.floor(ts / 1000) : ts
|
||||
return dayjs.unix(sec).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
function parseImFriendly(row: any): FriendlyParse | null {
|
||||
const raw = (row.text || '').trim()
|
||||
if (!raw) return null
|
||||
const looksLikeBizJson =
|
||||
raw.startsWith('{') && (/\bbusinessID\b/.test(raw) || /\bcmd\b/.test(raw))
|
||||
if (row.msg_type === 'custom' || looksLikeBizJson) {
|
||||
return parseImBusinessPayload(raw)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function senderLabel(row: any) {
|
||||
if (row.is_from_doctor) {
|
||||
return row.from_staff_name ? `医生(${row.from_staff_name})` : '医生/员工'
|
||||
}
|
||||
return patientName.value ? `患者(${patientName.value})` : '患者'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.diagnosisId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = (await getImChatMessages({
|
||||
diagnosis_id: props.diagnosisId,
|
||||
only_archived: 1
|
||||
})) as {
|
||||
lists?: any[]
|
||||
patient_im_id?: string
|
||||
patient_name?: string
|
||||
}
|
||||
rawRows.value = res?.lists || []
|
||||
patientImId.value = res?.patient_im_id || ''
|
||||
patientName.value = res?.patient_name || ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
rawRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reloadArchived() {
|
||||
load()
|
||||
}
|
||||
|
||||
async function triggerSync() {
|
||||
if (!props.diagnosisId) return
|
||||
syncing.value = true
|
||||
try {
|
||||
await triggerImChatSync({ diagnosis_id: props.diagnosisId })
|
||||
ElMessage.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('发起同步失败')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.diagnosisId,
|
||||
() => {
|
||||
load()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({ refresh: reloadArchived })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.im-chat-record-panel {
|
||||
min-height: 200px;
|
||||
}
|
||||
.panel-tip {
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
font-size: 13px;
|
||||
code {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chat-wrap {
|
||||
min-height: 120px;
|
||||
}
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: min(60vh, 520px);
|
||||
overflow-y: auto;
|
||||
padding: 4px 8px 12px;
|
||||
}
|
||||
.chat-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 88%;
|
||||
&.from-patient {
|
||||
align-self: flex-start;
|
||||
.bubble {
|
||||
background: var(--el-fill-color-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
&.from-doctor {
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
.meta {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.bubble {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
.name {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
.bubble {
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.chat-img {
|
||||
max-width: 240px;
|
||||
max-height: 200px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.friendly-text {
|
||||
.friendly-main {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.friendly-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+2005
-2005
File diff suppressed because it is too large
Load Diff
+2766
-2766
File diff suppressed because it is too large
Load Diff
+2417
-2417
File diff suppressed because it is too large
Load Diff
+1983
-1983
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user