This commit is contained in:
左哥 2025-06-08 22:58:26 +08:00
parent 1f0c13f60d
commit 6456393d40
10 changed files with 506 additions and 33 deletions

View File

@ -22,10 +22,11 @@
"@wangeditor/editor-for-vue": "next",
"axios": "^1.4.0",
"default-passive-events": "^2.0.0",
"echarts": "^5.6.0",
"element-plus": "^2.8.0",
"js-md5": "^0.8.3",
"mitt": "^3.0.1",
"moment": "^2.29.4",
"js-md5": "^0.8.3",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",

View File

@ -9,3 +9,43 @@ export const users = (data: any) => {
params: data
})
}
// 导出
export const exportUsers = (data: any) => {
return request({
url: `admin/patients/export`,
method: 'get',
responseType: 'blob',
params: data
})
}
export const exportUsersBasic = (id: any) => {
return request({
url: `admin/patient/basic/${id}`,
method: 'get',
})
}
export const questionnaires = (id: any) => {
return request({
url: `admin/patient/questionnaires/${id}`,
method: 'get',
})
}
export const chat = (id: any) => {
return request({
url: `admin/patient/chat/${id}`,
method: 'get',
})
}
export const questionnaire_info = (data: any) => {
return request({
url: `admin/patient/questionnaire_info`,
method: 'post',
data
})
}

1
src/assets/echarts/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,66 @@
<template>
<div class="my-echats" :id="id"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { useRoute } from 'vue-router'
const route = useRoute()
const myEcharts = ref(null)
const props = defineProps({
id: {
type: String,
default: ''
},
dataX: {
type: Array,
default: () => []
},
label: {
type: String,
default: ''
}
})
onMounted(() => {
console.log(props)
var chartDom = document.getElementById(props.id);
var myChart = echarts.init(chartDom);
var option;
option = {
grid: {
left: '50',
top: '30',
right: '30',
bottom: '30'
},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
yAxis: {
type: 'value'
},
series: [
{
name: props.label,
data: props.dataX,
type: 'line'
}
]
};
option && myChart.setOption(option);
})
</script>
<style scoped>
.my-echats {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="my-echats" :id="id"></div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { useRoute } from 'vue-router'
const route = useRoute()
const myEcharts = ref(null)
const props = defineProps({
id: {
type: String,
default: ''
},
actualHeight: {
type: Array,
default: () => []
},
normalHeightMin: {
type: Array,
default: () => []
},
normalHeightMax: {
type: Array,
default: () => []
},
months: {
type: Array,
default: () => ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
legendData: {
type: Array,
default: () => []
},
})
onMounted(() => {
var chartDom = document.getElementById(props.id);
var myChart = echarts.init(chartDom);
var option;
option = {
grid: {
left: '50',
top: '30',
right: '30',
bottom: '30'
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
result += param.seriesName + ': ' + param.value + ' cm<br/>';
});
return result;
}
},
legend: {
data: props.legendData
},
xAxis: {
type: 'category',
data: props.months
},
yAxis: {
type: 'value',
nameTextStyle: {
padding: [0, 0, 0, 40]
}
},
series: [
{
name: props.legendData[0],
data: props.actualHeight,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#409EFF'
}
},
{
name: props.legendData[1],
type: 'custom',
renderItem: function (params, api) {
const points = [];
const xPoints = [];
const minPoints = [];
const maxPoints = [];
for (let i = 0; i < props.months.length; i++) {
const x = api.coord([i, 0])[0];
const minY = api.coord([0, props.normalHeightMin[i]])[1];
const maxY = api.coord([0, props.normalHeightMax[i]])[1];
xPoints.push(x);
minPoints.push(minY);
maxPoints.push(maxY);
}
return {
type: 'group',
children: [{
type: 'polygon',
shape: {
points: xPoints.map((x, i) => [x, minPoints[i]]).concat(
xPoints.slice().reverse().map((x, i) => [x, maxPoints[maxPoints.length - 1 - i]])
)
},
style: {
fill: 'rgba(144, 238, 144, 0.3)'
}
}]
};
},
data: props.months
}
]
};
option && myChart.setOption(option);
})
</script>
<style scoped>
.my-echats {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,12 +1,13 @@
<template>
<header>
<div class="logo" @click="goHome">
<!-- <img src="@/assets/images/logo.svg" /> -->
<!-- 患者管理系统 -->
<div class="logo" >
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-if="route.meta.parent" :to=" route.meta.parentPath ">{{route.meta.parent}}</el-breadcrumb-item>
<el-breadcrumb-item v-if="route.meta.title">{{ route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="tab">
</div>
<Setting></Setting>
</header>
</template>
@ -16,6 +17,7 @@ import { useRouter, useRoute } from 'vue-router'
import { GET_TOKEN } from '@/utils/token';
import Setting from '@/layout/setting/index.vue'
import emitter from '@/eventBus'
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
const router = useRouter()
const route = useRoute()
@ -84,10 +86,12 @@ header {
.logo {
float: left;
cursor: pointer;
display: flex;
align-items: center;
height: 100%;
img {
height: 36px;
vertical-align: middle
:deep(.el-breadcrumb) {
line-height: normal;
}
}

View File

@ -20,7 +20,9 @@ const constantRoutes = [
name: 'user',
component: () => import('@/views/user/index.vue'),
meta: {
title: '患者管理'
title: '患者管理',
parent: '',
parentPath: '',
},
},
{
@ -28,7 +30,9 @@ const constantRoutes = [
name: 'userDetail',
component: () => import('@/views/user/userDetail.vue'),
meta: {
title: '患者详情'
title: '患者详情',
parent: '患者管理',
parentPath: '/user',
},
},
{
@ -36,7 +40,9 @@ const constantRoutes = [
name: 'news',
component: () => import('@/views/news/index.vue'),
meta: {
title: '文章管理'
title: '文章管理',
parent: '',
parentPath: '',
},
},
{
@ -44,7 +50,9 @@ const constantRoutes = [
name: 'addNew',
component: () => import('@/views/news/addNew.vue'),
meta: {
title: '添加文章'
title: '添加文章',
parent: '文章管理',
parentPath: '/news',
},
},
],

View File

@ -11,6 +11,7 @@
value-format="YYYY-MM-DD"
@change="handleDateChange"
clearable
style="margin-right: 10px;"
/>
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>

View File

@ -26,7 +26,7 @@
</div>
<div>
<el-button type="primary" @click="handleSearch">导出</el-button>
<el-button type="primary" @click="handleExport">导出</el-button>
</div>
<div class="user-table">
<el-table :data="tableData" style="width: 100%">
@ -70,7 +70,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import { users } from '@/api/user';
import { users, exportUsers } from '@/api/user';
import { useRouter } from 'vue-router';
const router = useRouter()
const tableData = ref([
@ -122,6 +122,22 @@ const handleReset = () => {
}
getList()
}
const handleExport = () => {
exportUsers(query.value).then(res => {
console.log(res)
// Blob
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
//
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
//
link.download = `用户数据_${new Date().getTime()}.xlsx`
//
link.click()
// URL
window.URL.revokeObjectURL(link.href)
})
}
onMounted(() => {
getList()
})

View File

@ -1,37 +1,241 @@
<template>
<div>
<div class="user-detail">
<el-descriptions title="患者信息">
<el-descriptions-item label="姓名">kooriookami</el-descriptions-item>
<el-descriptions-item label="性别">18100000000</el-descriptions-item>
<el-descriptions-item label="年龄">苏州市</el-descriptions-item>
<el-descriptions-item label="账号"> </el-descriptions-item>
<el-descriptions-item label="胆道闭锁手术时间">江苏省苏州市吴中区吴中大道 1188 </el-descriptions-item>
<el-descriptions-item label="术后时长"> </el-descriptions-item>
<el-descriptions-item label="当前风险"> </el-descriptions-item>
<el-descriptions-item label="生长曲线"> </el-descriptions-item>
<el-descriptions-item label="下次随访时间"> </el-descriptions-item>
<el-descriptions-item label="姓名">{{ userInfo.username }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ userInfo.sex === 1 ? '男' : '女' }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ userInfo.age }}</el-descriptions-item>
<el-descriptions-item label="账号">{{ userInfo.mobile }} </el-descriptions-item>
<el-descriptions-item label="胆道闭锁手术时间">{{ userInfo.operative_date }} </el-descriptions-item>
<el-descriptions-item label="术后时长">{{ userInfo.postoperative_duration }} </el-descriptions-item>
<el-descriptions-item label="当前风险">{{ userInfo.risk_value }} </el-descriptions-item>
<el-descriptions-item label="生长曲线">
<el-tag v-if="userInfo.growth_curve_type === 0" type="">未知</el-tag>
<el-tag v-if="userInfo.growth_curve_type === 1" type="warning">轻度偏高</el-tag>
<el-tag v-if="userInfo.growth_curve_type === 2" type="success">正常</el-tag>
<el-tag v-if="userInfo.growth_curve_type === 3" type="danger">重度偏高</el-tag>
</el-descriptions-item>
<el-descriptions-item label="下次随访时间">{{ userInfo.next_follow_date }} </el-descriptions-item>
</el-descriptions>
<div class="title">
<h2>随访记录</h2>
</div>
<div class="table">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="随访周期" />
<el-table-column prop="name" label="随访日期" />
<el-table-column prop="name" label="随访医院" />
<el-table-column label="操作">
<el-table :data="tableData" style="width: 100%" v-loading="isLoading">
<el-table-column prop="follow_name" label="随访周期" />
<el-table-column prop="follow_date" label="随访日期" />
<el-table-column prop="follow_hospital" label="随访医院" />
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="primary" @click="handleCheck(scope.row)" size="small">查看问卷</el-button>
<el-link type="primary" @click="handleCheck(scope.row)" size="small">查看问卷</el-link>
</template>
</el-table-column>
</el-table>
</div>
<div class="title">
<h2>身高曲线</h2>
</div>
<div class="echats" v-loading="isLoading">
<myEchartsSh v-if="!isLoading" ref="heightEcharts" id="heightEcharts"
:legendData="['实际身高', '正常身高范围']"
:actual-height="chatData.height"
:normal-height-min="[70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81]"
:normal-height-max="[80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91]" :label="'身高'" />
</div>
<div class="title">
<h2>体重曲线</h2>
</div>
<div class="echats" v-loading="isLoading">
<myEchartsSh v-if="!isLoading" ref="weightEcharts" id="weightEcharts"
:legendData="['实际体重', '正常体重范围']"
:actual-height="chatData.weight"
:normal-height-min="[70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81]"
:normal-height-max="[80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91]" :label="'体重'" />
</div>
<div class="title">
<h2>总胆红素趋势</h2>
</div>
<div class="echats" v-loading="isLoading">
<myEcharts v-if="!isLoading" ref="totalBilirubinEcharts" id="totalBilirubinEcharts"
:dataX="chatData.totalBilirubin" :label="'总胆红素'" />
</div>
<div class="title">
<h2>直接胆红素趋势</h2>
</div>
<div class="echats" v-loading="isLoading">
<myEcharts v-if="!isLoading" ref="directBilirubinEcharts" id="directBilirubinEcharts"
:dataX="chatData.directBilirubin" :label="'直接胆红素'" />
</div>
<el-dialog v-model="dialogVisible" title="" width="850px">
<div style="padding-left: 20px;">
<el-descriptions title="随访记录" :column="3">
<el-descriptions-item label="随访名称">{{ sfData.follow_name }}</el-descriptions-item>
<el-descriptions-item label="随访医院">{{ sfData.follow_hospital }}</el-descriptions-item>
<el-descriptions-item label="随访日期">{{ sfData.follow_date }}</el-descriptions-item>
<el-descriptions-item label="身高(CM)">{{ sfData.height }}</el-descriptions-item>
<el-descriptions-item label="体重(KG)">{{ sfData.weight }}</el-descriptions-item>
<el-descriptions-item label="头围(CM)">{{ sfData.head_circumference }}</el-descriptions-item>
<el-descriptions-item label="上臀围(CM)">{{ sfData.high_hip }}</el-descriptions-item>
<el-descriptions-item label="总胆红素(µmol/L)">{{ sfData.total_bilirubin }}</el-descriptions-item>
<el-descriptions-item label="直接胆红素(µmol/L)">{{ sfData.direct_bilirubin }}</el-descriptions-item>
<el-descriptions-item label="总胆汁酸(g/L)">{{ sfData.total_bile_acid }}</el-descriptions-item>
<el-descriptions-item label="谷丙U/L">{{ sfData.gu_bing }}</el-descriptions-item>
<el-descriptions-item label="谷草U/L">{{ sfData.grain_grass }}</el-descriptions-item>
<el-descriptions-item label="GGTU/L">{{ sfData.ggt }}</el-descriptions-item>
<el-descriptions-item label="ALPU/L">{{ sfData.alp }}</el-descriptions-item>
<el-descriptions-item label="白蛋白g/L">{{ sfData.albumin }}</el-descriptions-item>
<el-descriptions-item label="白细胞 (10^9/L)">{{ sfData.white_blood_cells }}</el-descriptions-item>
<el-descriptions-item label="血红蛋白 (g/L)">{{ sfData.hemoglobin }}</el-descriptions-item>
<el-descriptions-item label="血小板10^9/L)">{{ sfData.platelets }}</el-descriptions-item>
<el-descriptions-item label="凝血酶原时间s">{{ sfData.prothrombin_time }}</el-descriptions-item>
<el-descriptions-item label="活化的部分凝血酶时间s">{{ sfData.activated_partial_thrombin_time
}}</el-descriptions-item>
<el-descriptions-item label="国际标准化比">{{ sfData.international_normalized_ratio
}}</el-descriptions-item>
<el-descriptions-item label="肝肋下(mm)">{{ sfData.under_the_liver_rib }}</el-descriptions-item>
<el-descriptions-item label="肝剑突下(mm)">{{ sfData.under_the_xiphoid_liver }}</el-descriptions-item>
<el-descriptions-item label="脾肋下(mm)">{{ sfData.spleen_rib_area }}</el-descriptions-item>
<el-descriptions-item label="门静脉主干(mm)">{{ sfData.main_portal_vein }}</el-descriptions-item>
<el-descriptions-item label="门静脉流速">{{ sfData.pvv }}</el-descriptions-item>
<el-descriptions-item label="肝弹性值">{{ sfData.liver_elasticity_value }}</el-descriptions-item>
<el-descriptions-item label="有无腹水">{{ sfData.is_have_ascites === 1 ? '是' :
'否' }}</el-descriptions-item>
<el-descriptions-item label="有无肝囊肿">{{ sfData.is_have_cyst === 1 ? '是' : '否'
}}</el-descriptions-item>
<el-descriptions-item label="维生素A (ng/ml)">{{ sfData.vitamin_a }}</el-descriptions-item>
<el-descriptions-item label="维生素E (ng/ml)">{{ sfData.vitamin_e }}</el-descriptions-item>
<el-descriptions-item label="维生素K (ng/ml)">{{ sfData.vitamin_k }}</el-descriptions-item>
<el-descriptions-item label="25OHD3 (ng/ml)">{{ sfData.oh_d3 }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="检查报告" :column="1">
<el-descriptions-item label="B超报告">
<el-image v-for="(url, index) in sfData.b_mode_image?.split(',')" :key="index" :src="url"
:preview-src-list="sfData.b_mode_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
<el-descriptions-item label="血常规检查报告">
<el-image v-for="(url, index) in sfData.blood_routine_image?.split(',')" :key="index" :src="url"
:preview-src-list="sfData.blood_routine_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
<el-descriptions-item label="凝血功能检查报告">
<el-image v-for="(url, index) in sfData.coagulation_function_image?.split(',')" :key="index"
:src="url" :preview-src-list="sfData.coagulation_function_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
<el-descriptions-item label="肝功能检查报告">
<el-image v-for="(url, index) in sfData.liver_function_image?.split(',')" :key="index"
:src="url" :preview-src-list="sfData.liver_function_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
<el-descriptions-item label="营养指标检查报告">
<el-image v-for="(url, index) in sfData.nutritional_indicator_image?.split(',')" :key="index"
:src="url" :preview-src-list="sfData.nutritional_indicator_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
<el-descriptions-item label="MDT电子病历">
<el-image v-for="(url, index) in sfData.mdt_image?.split(',')" :key="index" :src="url"
:preview-src-list="sfData.mdt_image?.split(',')"
style="width: 100px; height: 100px; margin-right: 10px" />
</el-descriptions-item>
</el-descriptions>
</div>
<template v-slot:footer>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { exportUsersBasic, questionnaires, chat, questionnaire_info } from '@/api/user'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import myEcharts from '@/components/myEcahts.vue'
import myEchartsSh from '@/components/myEcahtsSh.vue'
const route = useRoute()
const id = ref('')
const tableData = ref([])
const userInfo = ref({})
const isLoading = ref(true)
const chatData = ref({
height: [],
weight: [],
totalBilirubin: [],
directBilirubin: []
})
const getTableData = async () => {
try {
isLoading.value = true
const [basicRes, questionnairesRes, chatRes] = await Promise.all([
exportUsersBasic(id.value),
questionnaires(id.value),
chat(id.value)
])
userInfo.value = basicRes
tableData.value = questionnairesRes.list
chatData.value = {
height: [],
weight: [],
totalBilirubin: [],
directBilirubin: []
}
chatRes.list.forEach(item => {
chatData.value.height.push(item.height)
chatData.value.weight.push(item.weight)
chatData.value.totalBilirubin.push(item.total_bilirubin)
chatData.value.directBilirubin.push(item.direct_bilirubin)
})
} catch (error) {
console.error('Error fetching data:', error)
} finally {
isLoading.value = false
}
}
const dialogVisible = ref(false)
const sfData = ref({})
const handleCheck = async (row) => {
const res = await questionnaire_info({ questionnaire_id: row.id, patient_id: Number(id.value) })
sfData.value = res
dialogVisible.value = true
}
onMounted(() => {
id.value = route.query.id
getTableData()
})
</script>
<style scoped></style>
<style scoped>
.user-detail {
height: 100%;
overflow-y: auto;
}
.title {
color: var(--el-text-color-primary);
font-size: 16px;
font-weight: bold;
margin-top: 30px;
margin-bottom: 10px;
}
.echats {
height: 400px;
}
.el-image {
vertical-align: top;
}
</style>