跟踪备注

This commit is contained in:
2026-05-05 17:52:35 +08:00
parent 919d0f856e
commit c830008dfc
14 changed files with 475 additions and 5 deletions
@@ -0,0 +1,179 @@
<template>
<div class="tracking-timeline-wrap">
<!-- 编辑模式输入新备注 -->
<div v-if="!readonly && diagnosisId" class="timeline-input">
<el-input
v-model="draft"
type="textarea"
:rows="2"
placeholder="输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录"
maxlength="1000"
show-word-limit
resize="none"
:disabled="saving"
/>
<div class="input-actions">
<el-button
type="primary"
size="small"
:loading="saving"
:disabled="!draft.trim()"
@click="handleSave"
>
添加
</el-button>
</div>
</div>
<!-- 时间轴 -->
<div v-if="notes.length" class="tracking-timeline">
<div v-for="note in notes" :key="note.id" class="timeline-node">
<div class="timeline-dot"></div>
<div class="timeline-date">{{ note.note_date }}</div>
<div class="timeline-body">
<div v-if="note.content" class="timeline-content">
<div
v-for="(line, idx) in splitLines(note.content)"
:key="idx"
class="content-line"
>
{{ line }}
</div>
</div>
</div>
</div>
</div>
<el-empty v-if="!notes.length && readonly" description="暂无跟踪备注" :image-size="48" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { diagnosisAddTrackingNote } from '@/api/tcm'
import feedback from '@/utils/feedback'
export interface TrackingNoteItem {
id: number
note_date: string
content: string | null
}
const props = defineProps<{
notes: TrackingNoteItem[]
diagnosisId?: number
readonly?: boolean
}>()
const emit = defineEmits<{ refresh: [] }>()
const draft = ref('')
const saving = ref(false)
const splitLines = (content: string | null): string[] => {
if (!content) return []
return content.split('\n').filter(Boolean)
}
const handleSave = async () => {
if (!props.diagnosisId) return
const content = draft.value.trim()
if (!content) return
saving.value = true
try {
await diagnosisAddTrackingNote({
diagnosis_id: props.diagnosisId,
tracking_content: content,
})
feedback.msgSuccess('已添加')
draft.value = ''
emit('refresh')
} finally {
saving.value = false
}
}
</script>
<style scoped lang="scss">
.tracking-timeline-wrap {
display: flex;
flex-direction: column;
gap: 14px;
}
.timeline-input {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 12px;
border-bottom: 1px dashed #ebeef5;
}
.input-actions {
display: flex;
justify-content: flex-end;
}
.tracking-timeline {
position: relative;
padding-left: 22px;
}
.tracking-timeline::before {
content: '';
position: absolute;
left: 6px;
top: 4px;
bottom: 4px;
width: 2px;
background: #ebeef5;
border-radius: 1px;
}
.timeline-node {
position: relative;
padding-bottom: 16px;
}
.timeline-node:last-child {
padding-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -19px;
top: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #16a34a;
border: 2px solid #dcfce7;
}
.timeline-date {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
font-family: 'IBM Plex Mono', Consolas, monospace;
}
.timeline-body {
display: flex;
flex-direction: column;
gap: 6px;
}
.timeline-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.content-line {
font-size: 12.5px;
color: #606266;
line-height: 1.6;
word-break: break-word;
}
</style>