Работа над журналом для ст. мед сестер

This commit is contained in:
brusnitsyn
2026-05-04 17:11:16 +09:00
parent f107ebd167
commit 7a58812072
61 changed files with 3532 additions and 1163 deletions

View File

@@ -61,10 +61,13 @@ watch(() => [props.minH, props.maxH], ([minH, maxH]) => {
<template>
<NFormItem :show-label="hasHeaderInOutside" :label="header" :show-feedback="hasFeedback" :feedback="feedback">
<NCard :class="noPadding ? 'no-padding h-full' : ''">
<template v-if="!hasHeaderInOutside" #header>
<NCard :class="noPadding ? 'no-padding h-full' : 'h-full'">
<template v-if="!hasHeaderInOutside && header" #header>
{{ header }}
</template>
<template #header-extra>
<slot name="header-extra" />
</template>
<NScrollbar :style="styles">
<slot />
</NScrollbar>

View File

@@ -0,0 +1,60 @@
<script setup>
import {NRadio} from 'naive-ui'
const props = defineProps({
title: { type: String },
description: { type: String, default: null }
})
</script>
<template>
<NRadio value="pro" class="radio-card">
<div class="card-content">
<strong class="card-title">{{ title }}</strong>
<span v-if="description">{{ description }}</span>
</div>
</NRadio>
</template>
<style scoped>
/* Убираем стандартные отступы Naive UI */
.radio-card {
margin-right: 0 !important;
margin-bottom: 0 !important;
}
/* Скрываем стандартный радио-кружок */
.radio-card :deep(.n-radio__action) {
display: none;
}
/* Превращаем сам n-radio в карточку */
.radio-card {
display: flex;
align-items: center;
border: 1px solid var(--n-text-color-disabled);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 200px;
flex: 1;
}
.radio-card:hover {
border-color: var(--n-dot-color-active);
}
/* Состояние "выбрано" */
.radio-card.n-radio--checked {
border-color: var(--n-dot-color-active);
}
/* Отключаем выделение текста внутри карточки */
.card-content {
pointer-events: none;
user-select: none;
display: flex;
flex-direction: column;
padding-left: 8px;
}
</style>

View File

@@ -0,0 +1,24 @@
<script setup>
import {computed} from "vue";
import {NTag} from 'naive-ui'
const props = defineProps({
urgencyId: {
type: Number,
default: 0
}
})
const typeForUrgency = computed(() => props.urgencyId === 1 ? 'success' : 'error')
const textForUrgency = computed(() => props.urgencyId === 1 ? 'Планово' : 'Экстренно')
</script>
<template>
<NTag :type="typeForUrgency" round :bordered="false" size="small">
{{ textForUrgency }}
</NTag>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,204 @@
<script setup>
import {NModal, NSteps, NStep, NTabs, NTabPane, NFlex, NGrid, NGi, NButton, NRadioGroup, NSelect, NInput, NFormItemGi, NDatePicker} from 'naive-ui'
import {computed, ref, watch} from "vue";
import AppRadio from "../../../Components/AppRadio.vue";
import {useDebounceFn} from "@vueuse/core";
import {format} from "date-fns";
const show = defineModel('show', { default: false })
const currentStep = ref(1)
const currentStatus = ref('process')
const isFinallyStep = computed(() => currentStep.value === 2)
const disableFillable = ref(false)
const form = ref({
patient_source: 'mis',
id: null,
medical_card_number: null,
full_name: '',
urgency_id: 1,
visit_result_id: 0,
birth_date: null,
recipient_date: new Date(),
death_date: null,
extract_date: null
})
const resetForm = () => {
form.value = {
...form.value,
id: null,
medical_card_number: null,
full_name: '',
urgency_id: 1,
visit_result_id: 0,
birth_date: null,
recipient_date: new Date(),
death_date: null,
extract_date: null
}
}
const urgencyOptions = [
{
label: '0 - Не определено',
value: 0
},
{
label: '1 - Планово',
value: 1
},
{
label: '2 - Экстренно',
value: 2
}
]
const visitResultOptions = [
{
label: '0 - Не определено',
value: 0
},
{
label: '1 - Выписан',
value: 1
}
]
const prev = () => {
if (currentStep.value === 0)
currentStep.value = null
else currentStep.value--
}
const next = () => {
if (currentStep.value === null)
currentStep.value = 1
else currentStep.value++
}
const submit = () => {
axios.post('/api/nurse/patients', {
...form.value
}).then(res => {
console.log(res)
})
}
const searchOptions = ref([])
const debounceSearch = useDebounceFn((s) => {
if (s.length === 0 || s.length === 1) return
search(s)
}, 1000)
const search = (search) => {
axios.post('/api/nurse/patients/search', {
search
}).then(res => {
searchOptions.value = res.data
})
}
const onChangeSearch = (historyId) => {
axios.get(`/api/nurse/patients/${historyId}`).then(res => {
form.value.medical_card_number = res.data.medical_card_number
form.value.full_name = res.data.full_name
form.value.urgency_id = res.data.urgency_id
form.value.visit_result_id = res.data.visit_result_id
form.value.birth_date = res.data.birth_date ? format(new Date(res.data.birth_date), 'yyyy-MM-dd HH:mm:ss') : null
form.value.death_date = res.data.death_date ? format(new Date(res.data.death_date), 'yyyy-MM-dd HH:mm:ss') : null
form.value.extract_date = res.data.extract_date ? format(new Date(res.data.extract_date), 'yyyy-MM-dd HH:mm:ss') : null
form.value.recipient_date = res.data.recipient_date ? format(new Date(res.data.recipient_date), 'yyyy-MM-dd HH:mm:ss') : null
})
}
watch(() => currentStep.value, (val) => {
if (val === 1) resetForm()
})
</script>
<template>
<NModal v-model:show="show"
segmented
preset="card"
title="Добавление пациента"
class="max-w-xl min-h-[500px]"
draggable
>
<NSteps size="small" :current="currentStep" :status="currentStatus">
<NStep title="Тип пациента" description="Укажите тип пациента" />
<NStep title="Добавление пациента" description="Заполните базовую информацию" />
</NSteps>
<NTabs tab-class="hidden!" :value="currentStep">
<NTabPane :name="1" class="mt-4">
<NRadioGroup class="w-full space-y-2!" v-model:value="form.patient_source">
<AppRadio title="МИС" value="mis" description="Выберите, если пациента можно найти в системе МИС" />
<AppRadio title="Спец. контингент" value="special" description="Выберите, если пациента нет в системе МИС" />
</NRadioGroup>
</NTabPane>
<NTabPane :name="2">
<NGrid cols="2" x-gap="8">
<NFormItemGi v-if="form.patient_source === 'mis'" span="2" label="Поиск пациента">
<NSelect v-model:value="form.id"
filterable
placeholder="Найти пациента по ФИО"
remote
:options="searchOptions"
@search="debounceSearch"
@change="onChangeSearch"
/>
</NFormItemGi>
<NFormItemGi span="2" label="ФИО">
<NInput v-model:value="form.full_name" placeholder="Иванов Иван Иванович" />
</NFormItemGi>
<NFormItemGi span="2" label="№ карты">
<NInput v-model:value="form.medical_card_number" />
</NFormItemGi>
<NFormItemGi span="1" label="Срочность">
<NSelect filterable v-model:value="form.urgency_id" :options="urgencyOptions" />
</NFormItemGi>
<NFormItemGi span="1" label="Исход госпитализации">
<NSelect filterable v-model:value="form.visit_result_id" :options="visitResultOptions" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата рождения">
<NDatePicker v-model:value="form.birth_date" class="w-full" value-format="yyyy-MM-dd" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время госпитализации">
<NDatePicker v-model:value="form.recipient_date" type="datetime" class="w-full" value-format="yyyy-MM-dd HH:mm:ss" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время выписки">
<NDatePicker v-model:value="form.extract_date" type="datetime" class="w-full" value-format="yyyy-MM-dd HH:mm:ss" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время смерти">
<NDatePicker v-model:value="form.death_date" type="datetime" class="w-full" value-format="yyyy-MM-dd HH:mm:ss" />
</NFormItemGi>
</NGrid>
</NTabPane>
</NTabs>
<template #action>
<NGrid cols="2" justify="space-between">
<NGi>
<NButton v-if="currentStep > 1" secondary @click="prev">
Назад
</NButton>
</NGi>
<NGi>
<NFlex justify="end">
<NButton v-if="isFinallyStep" secondary type="primary" @click="submit">
Добавить
</NButton>
<NButton v-else secondary type="primary" @click="next">
Далее
</NButton>
</NFlex>
</NGi>
</NGrid>
</template>
</NModal>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,139 @@
<script setup>
import {
NModal,
NSteps,
NStep,
NTabs,
NTabPane,
NFlex,
NGrid,
NGi,
NButton,
NSpin,
NSelect,
NInput,
NFormItemGi,
NDatePicker
} from 'naive-ui'
import {computed, ref, watch} from "vue";
import AppRadio from "../../../Components/AppRadio.vue";
import axios from "axios";
const show = defineModel('show', { default: false })
const props = defineProps({
historyId: {
type: Number
}
})
const form = ref({
patient_source: 'mis',
patient_id: null,
full_name: '',
urgency_id: 1,
visit_result_id: null,
birth_date: null,
recipient_date: null,
death_date: null,
extract_date: null
})
const loading = ref(true)
const urgencyOptions = [
{
label: '0 - Не определено',
value: 0
},
{
label: '1 - Планово',
value: 1
},
{
label: '2 - Экстренно',
value: 2
}
]
const visitResultOptions = [
{
label: '0 - Не определено',
value: 0
},
{
label: '1 - Выписан',
value: 1
}
]
const submit = () => {
}
const fetchPatient = async (historyId) => {
loading.value = true
await axios.get(`/api/nurse/patients/${historyId}`)
.then(res => {
form.value.full_name = res.data.full_name
form.value.urgency_id = res.data.urgency_id
form.value.visit_result_id = res.data.visit_result_id
form.value.birth_date = res.data.birth_date
form.value.death_date = res.data.death_date
form.value.extract_date = res.data.extract_date
form.value.recipient_date = res.data.recipient_date
})
.finally((e) => {
loading.value = false
})
}
watch(() => props.historyId, async (newHistoryId, historyId) => {
await fetchPatient(newHistoryId)
})
</script>
<template>
<NModal v-model:show="show"
segmented
preset="card"
title="Редактирование пациента"
class="max-w-xl min-h-[500px] relative"
draggable
>
<div v-if="loading">
<NSpin class="absolute top-1/2 left-1/2 -translate-x-1/2" />
</div>
<NGrid v-else cols="2" x-gap="8">
<NFormItemGi span="2" label="ФИО">
<NInput v-model:value="form.full_name" placeholder="Иванов Иван Иванович" />
</NFormItemGi>
<NFormItemGi span="1" label="Срочность">
<NSelect filterable v-model:value="form.urgency_id" :options="urgencyOptions" />
</NFormItemGi>
<NFormItemGi span="1" label="Исход госпитализации">
<NSelect filterable v-model:value="form.visit_result_id" :options="visitResultOptions" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата рождения">
<NDatePicker v-model:value="form.birth_date" class="w-full" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время госпитализации">
<NDatePicker v-model:value="form.recipient_date" type="datetime" class="w-full" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время выписки">
<NDatePicker v-model:value="form.extract_date" type="datetime" class="w-full" />
</NFormItemGi>
<NFormItemGi span="1" label="Дата и время смерти">
<NDatePicker v-model:value="form.death_date" type="datetime" class="w-full" />
</NFormItemGi>
</NGrid>
<template v-if="!loading" #action>
<NFlex justify="end">
<NButton secondary type="primary" @click="submit">
Сохранить
</NButton>
</NFlex>
</template>
</NModal>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,168 @@
<script setup>
import AppLayout from "../../../Layouts/AppLayout.vue";
import {NFlex, NTag, NDataTable, NButton, NTabs, NTabPane} from 'naive-ui'
import AppContainer from "../../../Components/AppContainer.vue";
import AppPanel from "../../../Components/AppPanel.vue";
import DatePickerQuery from "../../../Components/DatePickerQuery.vue";
import UrgencyBadge from "../../../Components/UrgencyBadge.vue";
import {computed, h, ref, shallowRef} from "vue"
import {TbArrowMoveRight, TbArrowMoveUp, TbEdit, TbCirclePlus, TbPencil} from 'vue-icons-plus/tb'
import {useAuthStore} from "../../../Stores/auth.js";
import AddMedicalHistoryModal from "../Components/AddMedicalHistoryModal.vue";
import EditMedicalHistoryModal from "../Components/EditMedicalHistoryModal.vue";
const props = defineProps({
inDepartmentHistories: {
type: Array,
default: []
},
recipientHistories: {
type: Array,
default: []
},
dischargedHistories: {
type: Array,
default: []
},
deceasedHistories: {
type: Array,
default: []
},
transferredHistories: {
type: Array,
default: []
},
dates: {
type: Array,
default: []
}
})
const showAddMedicalHistoryModal = shallowRef(false)
const showEditMedicalHistoryModal = shallowRef(false)
const editHistoryId = ref(null)
const authStore = useAuthStore()
const userDepartment = authStore.userDepartment
const columns = [
{
title: 'ФИО',
key: 'full_name',
minWidth: 280,
maxWidth: 400,
resizable: true
},
{
title: 'Дата поступления',
key: 'latest_migration.ingoing_date',
minWidth: 180,
maxWidth: 180,
width: 180,
resizable: false
},
{
title: 'Срочность',
key: 'urgency_id',
render: (row) => {
return h(UrgencyBadge, {urgencyId: row.urgency_id})
}
},
{
title: '',
key: 'actions',
align: 'end',
render: (row) => {
return h(
NButton, { size: 'tiny', type: 'default', secondary: true, onClick: () => onClickEditButton(row.id) },
{ default: () => 'Редактировать', icon: () => h(TbPencil, { size: '18px' }) })
}
}
]
const onClickEditButton = (historyId) => {
showEditMedicalHistoryModal.value = true
editHistoryId.value = historyId
}
const formattedLabel = (word, count) => {
return `${word} (${count})`
}
</script>
<template>
<AppLayout>
<AppContainer>
<AppPanel>
<NFlex justify="space-between" align="center">
<NTag type="info" :bordered="false">
{{ userDepartment.name_full }}
</NTag>
<DatePickerQuery :date="dates" />
</NFlex>
</AppPanel>
<AppPanel header="Пациенты в отделении" header-include-body>
<template #header-extra>
<NButton secondary @click="showAddMedicalHistoryModal = true">
<template #icon>
<TbCirclePlus />
</template>
Добавить пациента
</NButton>
</template>
<NTabs type="line">
<NTabPane name="all"
:tab="formattedLabel('В отделении', inDepartmentHistories.length)"
>
<NDataTable :columns="columns"
:data="inDepartmentHistories"
table-layout="fixed"
max-height="calc(100vh - 375px)"
min-height="calc(100vh - 375px)"
/>
</NTabPane>
<NTabPane name="income" :tab="formattedLabel('Поступившие', recipientHistories.length)">
<NDataTable :columns="columns"
:data="recipientHistories"
table-layout="fixed"
max-height="calc(100vh - 375px)"
min-height="calc(100vh - 375px)"
/>
</NTabPane>
<NTabPane name="outcome" :tab="formattedLabel('Выписанные', dischargedHistories.length)">
<NDataTable :columns="columns"
:data="dischargedHistories"
table-layout="fixed"
max-height="calc(100vh - 375px)"
min-height="calc(100vh - 375px)"
/>
</NTabPane>
<NTabPane name="dead" :tab="formattedLabel('Умершие', deceasedHistories.length)">
<NDataTable :columns="columns"
:data="deceasedHistories"
table-layout="fixed"
max-height="calc(100vh - 375px)"
min-height="calc(100vh - 375px)"
/>
</NTabPane>
<NTabPane name="transfer" :tab="formattedLabel('Переведенные', transferredHistories.length)">
<NDataTable :columns="columns"
:data="transferredHistories"
table-layout="fixed"
max-height="calc(100vh - 375px)"
min-height="calc(100vh - 375px)"
/>
</NTabPane>
</NTabs>
</AppPanel>
</AppContainer>
<AddMedicalHistoryModal v-model:show="showAddMedicalHistoryModal" />
<EditMedicalHistoryModal v-model:show="showEditMedicalHistoryModal" :history-id="editHistoryId" />
</AppLayout>
</template>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
</style>