DetailSection.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <template>
  2. <div class="detail-section" v-if="selectedElderly">
  3. <div class="detail-header">
  4. <h2>{{ selectedElderly.name }}的详细信息</h2>
  5. <div class="header-actions">
  6. <el-button
  7. :type="showLocationMap ? 'default' : 'primary'"
  8. size="small"
  9. @click="toggleLocationMap"
  10. >
  11. <Icon :icon="showLocationMap ? 'mdi:format-list-bulleted' : 'mdi:map-marker'" />
  12. <span style="margin-left: 6px">{{ showLocationMap ? '返回详情' : '长者位置' }}</span>
  13. </el-button>
  14. </div>
  15. </div>
  16. <!-- 地图模式:替换下方全部内容 -->
  17. <div v-if="showLocationMap" class="map-full-wrapper">
  18. <ElderLocationMap :elder-id="selectedElderly.id" />
  19. </div>
  20. <!-- 详情模式:健康指标 + 设备监控 -->
  21. <template v-else>
  22. <!-- 健康指标 -->
  23. <div class="health-metrics">
  24. <h3>健康指标</h3>
  25. <div
  26. class="metrics-grid"
  27. v-if="selectedElderly.healthList && selectedElderly.healthList?.length"
  28. >
  29. <div
  30. class="metric-card"
  31. v-for="(metric, index) in selectedElderly.healthList"
  32. :key="index"
  33. >
  34. <div class="metric-icon">
  35. <Icon :icon="getHealthIcon(metric)" />
  36. </div>
  37. <div class="metric-info">
  38. <div class="metric-value">{{ metric.value }}{{ metric.unit }}</div>
  39. <div class="metric-name">{{ metric.name }}</div>
  40. </div>
  41. <div class="metric-trend" :class="metric.status.includes('警') ? 'warning' : 'normal'">
  42. {{ metric.status }}
  43. </div>
  44. </div>
  45. </div>
  46. <el-empty v-else description="暂无健康指标" :image-size="40" />
  47. </div>
  48. <!-- 设备监控 -->
  49. <div class="devices-section-large">
  50. <div class="devices-header">
  51. <h3>设备监控</h3>
  52. <el-button type="primary" size="large" @click="addDevice">
  53. <Icon icon="ep:plus" />
  54. <span>添加设备</span>
  55. </el-button>
  56. </div>
  57. <div
  58. class="devices-grid-large"
  59. v-if="selectedElderly.deviceList && selectedElderly.deviceList?.length"
  60. >
  61. <DeviceCard
  62. v-for="(device, index) in selectedElderly.deviceList"
  63. :key="index"
  64. :device="device"
  65. :selectedElderly="selectedElderly"
  66. :device-type-options="deviceTypeOptions"
  67. @show-detail="showDeviceDetail"
  68. @remove="removeDevice"
  69. />
  70. </div>
  71. <el-empty v-else description="暂无设备" :image-size="40" />
  72. </div>
  73. </template>
  74. </div>
  75. <div class="detail-placeholder" v-else>
  76. <div class="placeholder-content">
  77. <div class="placeholder-icon">
  78. <Icon icon="mdi:gesture-tap" />
  79. </div>
  80. <h3>请选择一位老人查看详细信息</h3>
  81. <p>点击左侧老人卡片查看健康数据和设备状态</p>
  82. </div>
  83. </div>
  84. </template>
  85. <script lang="ts" setup>
  86. import { defineAsyncComponent, ref } from 'vue'
  87. const DeviceCard = defineAsyncComponent(() => import('./DeviceCard.vue'))
  88. const ElderLocationMap = defineAsyncComponent(() => import('./ElderLocationMap.vue'))
  89. const showLocationMap = ref(false)
  90. const toggleLocationMap = () => {
  91. showLocationMap.value = !showLocationMap.value
  92. }
  93. interface HealthVO {
  94. name: string
  95. value: string
  96. status: string
  97. unit: string
  98. }
  99. interface DetailDevice {
  100. deviceType: string
  101. installPosition: string
  102. status: string
  103. indicatorText: string
  104. deviceCode: string
  105. }
  106. interface SelectElderly {
  107. id: number
  108. healthList: HealthVO[]
  109. name: string
  110. deviceList: DetailDevice[]
  111. }
  112. interface DeviceTypeVO {
  113. deviceType: string
  114. deviceTypeName: string
  115. displayOrder: number
  116. }
  117. interface Props {
  118. selectedElderly: SelectElderly | null
  119. deviceTypeOptions?: DeviceTypeVO[]
  120. }
  121. const props = defineProps<Props>()
  122. const emit = defineEmits<{
  123. (e: 'addDevice', elderly: SelectElderly): void
  124. (e: 'showDeviceDetail', device: DetailDevice): void
  125. (e: 'removeDevice', elderly: SelectElderly, device: DetailDevice): void
  126. }>()
  127. const healthIconMap: Record<string, string> = {
  128. 血氧: 'mdi:oxygen-tank',
  129. 心率: 'mdi:heart-pulse',
  130. 血压: 'mdi:blood-bag',
  131. 体温: 'mdi:thermometer',
  132. 血糖: 'mdi:needle',
  133. 血脂: 'mdi:blood-bag',
  134. 呼吸频率: 'mdi:lungs',
  135. 步数: 'mdi:walk',
  136. 睡眠: 'mdi:sleep',
  137. 体重: 'mdi:scale',
  138. 身高: 'mdi:human-male-height',
  139. BMI: 'mdi:human-male-height-variant',
  140. 运动量: 'mdi:run',
  141. 卡路里: 'mdi:fire',
  142. 水分摄入: 'mdi:cup-water',
  143. 血氧饱和度: 'mdi:oxygen-tank',
  144. 心电图: 'mdi:heart-flash',
  145. 肺功能: 'mdi:lungs',
  146. 骨密度: 'mdi:bone',
  147. 视力: 'mdi:eye',
  148. 听力: 'mdi:ear-hearing',
  149. 胆固醇: 'mdi:blood-bag',
  150. 尿酸: 'mdi:flask',
  151. 肝功能: 'mdi:liver',
  152. 肾功能: 'mdi:kidney',
  153. 血糖波动: 'mdi:chart-line',
  154. 血压波动: 'mdi:chart-areaspline',
  155. 心率变异性: 'mdi:chart-bell-curve',
  156. 睡眠时长: 'mdi:clock-sleep',
  157. 深睡时长: 'mdi:sleep',
  158. 浅睡时长: 'mdi:sleep',
  159. REM睡眠: 'mdi:sleep',
  160. 入睡时间: 'mdi:clock-start',
  161. 醒来时间: 'mdi:clock-end',
  162. 夜间醒来次数: 'mdi:alert-circle',
  163. 日间活动量: 'mdi:walk',
  164. 静息心率: 'mdi:heart',
  165. 最大心率: 'mdi:heart',
  166. 最低心率: 'mdi:heart',
  167. 收缩压: 'mdi:blood-bag',
  168. 舒张压: 'mdi:blood-bag',
  169. 平均血压: 'mdi:blood-bag',
  170. 血糖餐前: 'mdi:food',
  171. 血糖餐后: 'mdi:food',
  172. 血糖空腹: 'mdi:food-off',
  173. 血氧夜间: 'mdi:weather-night',
  174. 血氧日间: 'mdi:weather-sunny',
  175. 呼吸暂停: 'mdi:alert',
  176. 打鼾指数: 'mdi:volume-high',
  177. 体温晨起: 'mdi:weather-sunset-up',
  178. 体温晚间: 'mdi:weather-sunset-down',
  179. 体重变化: 'mdi:chart-line',
  180. BMI趋势: 'mdi:trending-up',
  181. 水分平衡: 'mdi:water-percent',
  182. 运动强度: 'mdi:run-fast',
  183. 卡路里消耗: 'mdi:fire',
  184. 睡眠效率: 'mdi:percent',
  185. 睡眠评分: 'mdi:star',
  186. 健康评分: 'mdi:star',
  187. 压力指数: 'mdi:alert',
  188. 情绪状态: ' mdi:emoticon-happy',
  189. 认知功能: 'mdi:brain',
  190. 平衡能力: 'mdi:scale-balance',
  191. 握力: 'mdi:hand-back-right',
  192. 步行速度: 'mdi:speedometer',
  193. 日常活动: 'mdi:home',
  194. 用药依从性: 'mdi:pill',
  195. 复诊提醒: 'mdi:calendar',
  196. 紧急呼叫: 'mdi:alert-circle'
  197. }
  198. const getHealthIcon = (metric: HealthVO) => {
  199. return healthIconMap[metric.name] || 'mdi:help-circle-outline'
  200. }
  201. const addDevice = () => {
  202. if (!props.selectedElderly) return
  203. emit('addDevice', props.selectedElderly)
  204. }
  205. const showDeviceDetail = (device: DetailDevice) => {
  206. emit('showDeviceDetail', device)
  207. }
  208. const removeDevice = (elderly: SelectElderly, device: DetailDevice) => {
  209. emit('removeDevice', elderly, device)
  210. }
  211. </script>
  212. <style lang="scss" scoped>
  213. $primary-color: #1a73e8;
  214. $text-light: #fff;
  215. $text-gray: #8a8f98;
  216. $success-color: #26de81;
  217. $warning-color: #fd9644;
  218. .detail-section {
  219. display: flex;
  220. padding: 25px;
  221. overflow-y: auto;
  222. background: rgb(26 31 46 / 85%);
  223. border: 1px solid rgb(255 255 255 / 12%);
  224. border-radius: 16px;
  225. flex-direction: column;
  226. gap: 25px;
  227. }
  228. .detail-header {
  229. display: flex;
  230. justify-content: space-between;
  231. align-items: center;
  232. padding-bottom: 15px;
  233. border-bottom: 1px solid rgb(255 255 255 / 10%);
  234. h2 {
  235. font-size: 28px;
  236. }
  237. }
  238. .map-full-wrapper {
  239. width: 100%;
  240. height: 600px;
  241. border-radius: 12px;
  242. overflow: hidden;
  243. }
  244. .health-metrics h3,
  245. .devices-section-large h3 {
  246. margin-bottom: 15px;
  247. font-size: 22px;
  248. }
  249. .devices-header {
  250. display: flex;
  251. justify-content: space-between;
  252. align-items: center;
  253. margin-bottom: 15px;
  254. }
  255. .metrics-grid {
  256. display: grid;
  257. grid-template-columns: repeat(2, 1fr);
  258. gap: 15px;
  259. margin-bottom: 20px;
  260. }
  261. .metric-card {
  262. display: flex;
  263. align-items: center;
  264. padding: 20px;
  265. background: rgb(255 255 255 / 5%);
  266. border-radius: 12px;
  267. gap: 15px;
  268. }
  269. .metric-icon {
  270. :deep(svg),
  271. :deep(span) {
  272. width: 32px !important;
  273. height: 32px !important;
  274. }
  275. }
  276. .metric-info {
  277. flex: 1;
  278. .metric-value {
  279. margin-bottom: 5px;
  280. font-size: 24px;
  281. font-weight: 600;
  282. }
  283. .metric-name {
  284. color: $text-gray;
  285. }
  286. }
  287. .metric-trend {
  288. padding: 5px 10px;
  289. font-size: 14px;
  290. border-radius: 20px;
  291. &.normal {
  292. color: $success-color;
  293. background: rgb(38 222 129 / 20%);
  294. }
  295. &.warning {
  296. color: $warning-color;
  297. background: rgb(253 150 68 / 20%);
  298. }
  299. }
  300. .devices-grid-large {
  301. display: grid;
  302. grid-template-columns: repeat(2, 1fr);
  303. gap: 20px;
  304. }
  305. .detail-placeholder {
  306. display: flex;
  307. align-items: center;
  308. justify-content: center;
  309. background: rgb(26 31 46 / 85%);
  310. border: 1px solid rgb(255 255 255 / 12%);
  311. border-radius: 16px;
  312. .placeholder-content {
  313. text-align: center;
  314. .placeholder-icon {
  315. margin-bottom: 20px;
  316. :deep(svg),
  317. :deep(span) {
  318. width: 32px !important;
  319. height: 32px !important;
  320. }
  321. }
  322. h3 {
  323. margin-bottom: 10px;
  324. font-size: 24px;
  325. }
  326. p {
  327. color: $text-gray;
  328. }
  329. }
  330. }
  331. .detail-section::-webkit-scrollbar {
  332. width: 8px;
  333. }
  334. .detail-section::-webkit-scrollbar-track {
  335. background: rgb(255 255 255 / 5%);
  336. border-radius: 4px;
  337. }
  338. .detail-section::-webkit-scrollbar-thumb {
  339. background: rgb(26 115 232 / 50%);
  340. border-radius: 4px;
  341. }
  342. </style>