home-refactored.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291
  1. <template>
  2. <div class="elderly-management-system large-screen">
  3. <div class="cyber-bg"></div>
  4. <div class="cyber-grid"></div>
  5. <div class="my-container">
  6. <!-- 顶部信息栏 -->
  7. <TopInfoBar :tenant-name="getTenantName()" />
  8. <!-- 右上角实时事件抽屉开关 -->
  9. <el-tooltip class="box-item" effect="dark" content="设备实时事件" placement="top-start">
  10. <button class="realtime-fab" @click="realtimeDrawerVisible = true" title="实时事件">
  11. <Icon icon="mdi:radar" :size="30" />
  12. </button>
  13. </el-tooltip>
  14. <!-- 统计信息卡片 -->
  15. <StatsCard :stats="largeScreenStats" @stat-card-click="handleStatCardClick" />
  16. <!-- 主内容区域 -->
  17. <div v-if="!showAllDevices" class="main-content-large">
  18. <!-- 左侧老人列表 -->
  19. <ElderlyList
  20. :elderly-list="elderlyList"
  21. :selected-elderly-id="selectedElderly.id"
  22. :warning-flags="warningFlags"
  23. @select-elderly="selectElderly"
  24. @add-device="openAddDeviceFromList"
  25. @handle-warning="openHandleWarningDialog"
  26. @add-elder="openAddElderDialog"
  27. />
  28. <!-- 右侧详情区域 -->
  29. <DetailSection
  30. ref="detailSectionRef"
  31. :selected-elderly="selectedElderly.id > 0 ? selectedElderly : null"
  32. :device-type-options="deviceTypeOptions"
  33. @add-device="openAddDeviceDialog"
  34. @show-device-detail="showDeviceDetail"
  35. @remove-device="removeDevice"
  36. @view-profile="openElderProfileDialog"
  37. @view-warning-service="openElderWarningServiceDialog"
  38. />
  39. </div>
  40. <!-- 全部设备视图(组件) -->
  41. <AllDevicesView
  42. v-else
  43. :devices="allDevices as any"
  44. @refresh="handelDeviceRefresh"
  45. @back="backToOverview"
  46. @show-device-detail="showDeviceDetailByCode"
  47. />
  48. <!-- 底部状态栏 -->
  49. <StatusBar
  50. :system-status="largeScreenStatsData.systemStatus"
  51. :last-time="largeScreenStatsData.lastTime"
  52. :has-alerts="hasAlerts"
  53. />
  54. </div>
  55. <!-- 对话框和抽屉 -->
  56. <AddDeviceDialog
  57. ref="addDeviceDialogRef"
  58. v-model:visible="dialogVisible"
  59. :current-elderly="currentElderly"
  60. :device-type-options="deviceTypeOptions"
  61. :tenant-name="getTenantName()"
  62. :organization-id="organizationId"
  63. @submit="addDevice"
  64. />
  65. <DeviceDetailDialog
  66. v-model:visible="deviceDetailVisible"
  67. :device="deviceDetail"
  68. :device-type-options="deviceTypeOptions"
  69. />
  70. <WarningDrawer
  71. v-model:visible="warningDrawerVisible"
  72. v-model:page-num="pageNum"
  73. v-model:page-size="pageSize"
  74. :warning-data="warningData"
  75. :total="total"
  76. @page-change="handlePageChange"
  77. @size-change="handleSizeChange"
  78. @handle-warning="onHandleWarningFromDrawer"
  79. />
  80. <AddElderDialog v-model:visible="addElderDialogVisible" @submit="submitAddElder" />
  81. <HandleWarningDialog
  82. v-model:visible="handleWarningDialogVisible"
  83. :current-elderly="currentWarningElderly"
  84. :mode="handleWarningMode"
  85. @submit="submitHandleWarning"
  86. />
  87. <ElderProfileDialog v-model="elderProfileDialogVisible" :elder-id="currentProfileElderId" />
  88. <ElderWarningServiceDialog
  89. v-model="elderWarningServiceDialogVisible"
  90. :elder-id="currentWarningServiceElderId"
  91. />
  92. <!-- 实时事件抽屉 -->
  93. <RealtimeFeedDrawer ref="realtimeDrawerRef" v-model="realtimeDrawerVisible" />
  94. </div>
  95. </template>
  96. <script lang="ts" setup>
  97. import { ref, computed, onMounted, onUnmounted, nextTick, h, defineAsyncComponent } from 'vue'
  98. import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
  99. import fetchHttp from '@/config/axios/fetchHttp'
  100. import { getAccessToken, getLoginForm } from '@/utils/auth'
  101. import { useWebSocket } from './composables/useWebSocket'
  102. import { amapReverseGeocode } from '@/utils/amapService'
  103. // 导入组件
  104. const TopInfoBar = defineAsyncComponent(() => import('./components/TopInfoBar.vue'))
  105. const StatsCard = defineAsyncComponent(() => import('./components/StatsCard.vue'))
  106. const ElderlyList = defineAsyncComponent(() => import('./components/ElderlyList.vue'))
  107. const DetailSection = defineAsyncComponent(() => import('./components/DetailSection.vue'))
  108. const AddDeviceDialog = defineAsyncComponent(() => import('./components/AddDeviceDialog.vue'))
  109. const DeviceDetailDialog = defineAsyncComponent(() => import('./components/DeviceDetailDialog.vue'))
  110. const WarningDrawer = defineAsyncComponent(() => import('./components/WarningDrawer.vue'))
  111. const AddElderDialog = defineAsyncComponent(() => import('./components/AddElderDialog.vue'))
  112. const HandleWarningDialog = defineAsyncComponent(
  113. () => import('./components/HandleWarningDialog.vue')
  114. )
  115. const StatusBar = defineAsyncComponent(() => import('./components/StatusBar.vue'))
  116. const AllDevicesView = defineAsyncComponent(() => import('./components/AllDevicesView.vue'))
  117. const ElderProfileDialog = defineAsyncComponent(() => import('./components/ElderProfileDialog.vue'))
  118. const ElderWarningServiceDialog = defineAsyncComponent(
  119. () => import('./components/ElderWarningServiceDialog.vue')
  120. )
  121. const RealtimeFeedDrawer = defineAsyncComponent(() => import('./components/RealtimeFeedDrawer.vue'))
  122. const realtimeDrawerVisible = ref(false)
  123. const realtimeDrawerRef = ref<any>(null)
  124. // 类型定义
  125. interface WarningHistory {
  126. happensAt: string
  127. content: string
  128. elderId?: number
  129. elderName?: string
  130. }
  131. interface StatisticsVO {
  132. systemStatus: string
  133. lastTime: string
  134. isWarning: boolean
  135. }
  136. interface CommonVo {
  137. name: string
  138. value: string
  139. }
  140. interface HistoryInfoVo {
  141. happensAt: string
  142. content: string
  143. }
  144. interface Elderly {
  145. id: number
  146. avatar: string
  147. name: string
  148. age: number
  149. gender: string
  150. healthStatus: string
  151. healthText: string
  152. deviceNumber: number
  153. elderPhone?: string
  154. relativePhone?: string
  155. _flashEffect?: boolean
  156. }
  157. interface HealthVO {
  158. name: string
  159. value: string
  160. status: string
  161. unit: string
  162. }
  163. interface DetailDevice {
  164. deviceType: string
  165. installPosition: string
  166. status: string
  167. indicatorText: string
  168. deviceCode: string
  169. }
  170. interface SelectElderly {
  171. id: number
  172. healthList: HealthVO[]
  173. name: string
  174. deviceList: DetailDevice[]
  175. }
  176. interface Device {
  177. deviceType: string
  178. installPosition: string
  179. status: string
  180. indicatorInfo: CommonVo[]
  181. historyInfo: HistoryInfoVo[]
  182. }
  183. interface LargeScreenStat {
  184. icon: string
  185. value: number | string
  186. label: string
  187. trend: 'up' | 'stable'
  188. change: string
  189. type?: 'warning'
  190. clickable?: boolean
  191. indicator: string
  192. }
  193. interface DeviceTypeVO {
  194. deviceType: string
  195. deviceTypeName: string
  196. displayOrder: number
  197. }
  198. // 常量定义
  199. const WARNING_STORAGE_KEY = 'elder_warning_flags'
  200. const organizationId = localStorage.getItem('organizationId') || ''
  201. // 响应式数据
  202. const total = ref(0)
  203. const pageNum = ref(1)
  204. const pageSize = ref(10)
  205. const elderlyList = ref<Elderly[]>([])
  206. const dialogVisible = ref(false)
  207. const addDeviceDialogRef = ref<any>(null)
  208. const detailSectionRef = ref<any>(null)
  209. const deviceDetailVisible = ref(false)
  210. const currentElderly = ref<SelectElderly | null>(null)
  211. const selectedElderly = ref<SelectElderly>({
  212. id: 0,
  213. name: '',
  214. healthList: [],
  215. deviceList: []
  216. })
  217. // 全部设备视图
  218. const showAllDevices = ref(false)
  219. interface AllDevice {
  220. deviceType: string
  221. deviceCode: string
  222. installPosition: string
  223. elderName: string
  224. historyInfo: HistoryInfoVo[]
  225. }
  226. const allDevices = ref<AllDevice[]>([])
  227. const deviceTypeOptions = ref<DeviceTypeVO[]>([])
  228. const warningDrawerVisible = ref(false)
  229. const warningData = ref<WarningHistory[]>([])
  230. const deviceDetail = ref<Device | null>(null)
  231. const largeScreenStatsData = ref<StatisticsVO>({
  232. systemStatus: '正常',
  233. lastTime: new Date().toLocaleString(),
  234. isWarning: false
  235. })
  236. const addElderDialogVisible = ref(false)
  237. const handleWarningDialogVisible = ref(false)
  238. const currentWarningElderly = ref<Elderly | null>(null)
  239. const handleWarningMode = ref<'default' | 'reportOnly'>('default')
  240. const warningFlags = ref<number[]>([])
  241. const elderProfileDialogVisible = ref(false)
  242. const currentProfileElderId = ref(0)
  243. const elderWarningServiceDialogVisible = ref(false)
  244. const currentWarningServiceElderId = ref(0)
  245. // 告警标记管理
  246. const loadWarningFlags = () => {
  247. try {
  248. const raw = localStorage.getItem(WARNING_STORAGE_KEY)
  249. warningFlags.value = raw ? JSON.parse(raw) : []
  250. } catch (e) {
  251. warningFlags.value = []
  252. }
  253. }
  254. const saveWarningFlags = () => {
  255. localStorage.setItem(WARNING_STORAGE_KEY, JSON.stringify(warningFlags.value))
  256. }
  257. const addWarningFlag = (elderId: number) => {
  258. if (!elderId) return
  259. if (!warningFlags.value.includes(elderId)) {
  260. warningFlags.value.push(elderId)
  261. saveWarningFlags()
  262. }
  263. }
  264. const clearWarningFlag = (elderId: number) => {
  265. const idx = warningFlags.value.indexOf(elderId)
  266. if (idx !== -1) {
  267. warningFlags.value.splice(idx, 1)
  268. saveWarningFlags()
  269. }
  270. }
  271. // 大屏统计信息
  272. const largeScreenStats = ref<LargeScreenStat[]>([
  273. {
  274. icon: 'mdi:account-group',
  275. value: 0,
  276. label: '老人数量',
  277. trend: 'up',
  278. change: '',
  279. clickable: true,
  280. indicator: 'elderCount'
  281. },
  282. {
  283. icon: 'mdi:devices',
  284. value: 0,
  285. label: '设备总数',
  286. trend: 'up',
  287. change: '',
  288. clickable: true,
  289. indicator: 'deviceCount'
  290. },
  291. // {
  292. // icon: 'mdi:shield-check',
  293. // value: 0,
  294. // label: '在线设备',
  295. // trend: 'up',
  296. // change: '100%',
  297. // indicator: 'onlineCount'
  298. // },
  299. {
  300. icon: 'mdi:alert-decagram',
  301. value: 0,
  302. label: '警告设备',
  303. trend: 'up',
  304. change: '需关注',
  305. type: 'warning',
  306. clickable: true,
  307. indicator: 'warningCount'
  308. }
  309. ])
  310. const hasAlerts = computed(() => largeScreenStatsData.value.isWarning)
  311. // 方法
  312. const getTenantName = () => {
  313. return getLoginForm()?.tenantName || ''
  314. }
  315. const openAddDeviceDialog = (elderly: SelectElderly) => {
  316. currentElderly.value = elderly
  317. dialogVisible.value = true
  318. nextTick(() => {
  319. // 确保子组件已挂载并接收到 props 后再初始化表单
  320. addDeviceDialogRef.value?.initForm?.()
  321. })
  322. }
  323. const openAddDeviceFromList = (elderly: Elderly) => {
  324. const temp: SelectElderly = {
  325. id: elderly.id,
  326. name: elderly.name,
  327. healthList: [],
  328. deviceList: []
  329. }
  330. openAddDeviceDialog(temp)
  331. }
  332. const addDevice = async (data: any) => {
  333. try {
  334. const res = await fetchHttp.post('/api/pc/admin/bindDevice', data, {
  335. headers: {
  336. Authorization: `Bearer ${getAccessToken()}`
  337. }
  338. })
  339. if (res) {
  340. ElMessage.success('设备添加成功!')
  341. getElderDeviceMessage(selectedElderly.value.id)
  342. } else {
  343. ElMessage.error('设备添加失败!')
  344. }
  345. } catch (error) {
  346. console.error('添加设备失败:', error)
  347. ElMessage.error('添加设备时出现错误')
  348. }
  349. }
  350. const removeDevice = (elderly: any, device: DetailDevice) => {
  351. console.log('elderly', elderly)
  352. console.log('device', device)
  353. ElMessageBox.confirm(`确定要删除设备吗?`, '删除确认', {
  354. confirmButtonText: '确定',
  355. cancelButtonText: '取消',
  356. type: 'warning'
  357. })
  358. .then(async () => {
  359. const res = await fetchHttp.post(
  360. '/api/pc/admin/unbindDevice',
  361. {
  362. deviceCode: device.deviceCode,
  363. elderId: Number(elderly.id)
  364. },
  365. {
  366. headers: {
  367. Authorization: `Bearer ${getAccessToken()}`
  368. }
  369. }
  370. )
  371. if (res) {
  372. ElMessage.success('设备删除成功!')
  373. getElderDeviceMessage(selectedElderly.value.id)
  374. } else {
  375. ElMessage.error('设备删除失败!')
  376. }
  377. })
  378. .catch(() => {})
  379. }
  380. const openAddElderDialog = () => {
  381. addElderDialogVisible.value = true
  382. }
  383. const openElderProfileDialog = (elderly: any) => {
  384. currentProfileElderId.value = elderly.id
  385. elderProfileDialogVisible.value = true
  386. }
  387. const openElderWarningServiceDialog = (elderly: any) => {
  388. currentWarningServiceElderId.value = elderly.id
  389. elderWarningServiceDialogVisible.value = true
  390. }
  391. const submitAddElder = async (data: any) => {
  392. try {
  393. const res = await fetchHttp.post('/api/pc/admin/addElder', data, {
  394. headers: {
  395. Authorization: `Bearer ${getAccessToken()}`
  396. }
  397. })
  398. if (res) {
  399. ElMessage.success('长者添加成功!')
  400. await getElderList()
  401. } else {
  402. ElMessage.error('长者添加失败!')
  403. }
  404. } catch (error) {
  405. console.error('添加长者失败:', error)
  406. ElMessage.error('添加长者时出现错误')
  407. }
  408. }
  409. const openHandleWarningDialog = (elderly: Elderly) => {
  410. currentWarningElderly.value = elderly
  411. handleWarningMode.value = 'default' // 老人卡片进入:默认电话回访
  412. handleWarningDialogVisible.value = true
  413. }
  414. const submitHandleWarning = async (data: any) => {
  415. // 电话回访与上报告警均可确认处理,并上送 elderId 与上报信息(remarks)
  416. const params = {
  417. elderId: data.elderId,
  418. remarks: data.message || ''
  419. }
  420. try {
  421. const res = await fetchHttp.post('/api/pc/admin/processAlert', params, {
  422. headers: {
  423. Authorization: `Bearer ${getAccessToken()}`
  424. }
  425. })
  426. if (res) {
  427. ElMessage.success(data.handleType === 'report' ? '告警情况已上报!' : '电话回访已确认!')
  428. clearWarningFlag(data.elderId)
  429. handleWarningDialogVisible.value = false
  430. if (selectedElderly.value.id === data.elderId) {
  431. await getElderDeviceMessage(selectedElderly.value.id)
  432. }
  433. } else {
  434. ElMessage.error('处理失败,请重试')
  435. }
  436. } catch (error) {
  437. console.error('处理告警失败:', error)
  438. ElMessage.error('处理告警时出现错误')
  439. }
  440. }
  441. const selectElderly = (elderly: Elderly) => {
  442. selectedElderly.value.id = elderly.id
  443. selectedElderly.value.name = elderly.name
  444. // clearWarningFlag(elderly.id)
  445. getElderDeviceMessage(selectedElderly.value.id)
  446. detailSectionRef.value?.reBackToDetail()
  447. }
  448. const showDeviceDetail = async (device: DetailDevice) => {
  449. await getDeviceDetail(device.deviceCode)
  450. deviceDetailVisible.value = true
  451. }
  452. const handleStatCardClick = (stat: LargeScreenStat) => {
  453. // 点击老人数量:回到总览视图
  454. if (stat.indicator === 'elderCount') {
  455. backToOverview()
  456. return
  457. }
  458. // 点击设备总数:切换到全部设备视图
  459. if (stat.indicator === 'deviceCount') {
  460. openAllDevicesView()
  461. return
  462. }
  463. // 点击警告设备:打开预警抽屉
  464. if (stat.indicator === 'warningCount' && Number(stat.value) > 0) {
  465. getAllWarning()
  466. warningDrawerVisible.value = true
  467. }
  468. }
  469. const handlePageChange = (newPageNum: number) => {
  470. pageNum.value = newPageNum
  471. getAllWarning()
  472. }
  473. const handleSizeChange = (newPageSize: number) => {
  474. pageSize.value = newPageSize
  475. pageNum.value = 1
  476. getAllWarning()
  477. }
  478. const onHandleWarningFromDrawer = (record: WarningHistory) => {
  479. let target: Elderly | null = null
  480. if ((record as any).elderId) {
  481. target = elderlyList.value.find((e) => e.id === (record as any).elderId) || null
  482. }
  483. if (!target && (record as any).elderName) {
  484. target = elderlyList.value.find((e) => e.name === (record as any).elderName) || null
  485. }
  486. if (!target && record.content) {
  487. // 尝试从内容中提取姓名(简单匹配)
  488. const match = record.content.match(/(长者|老人|用户)[::]\s*([\u4e00-\u9fa5A-Za-z0-9_]+)/)
  489. if (match && match[2]) {
  490. target = elderlyList.value.find((e) => e.name === match[2]) || null
  491. }
  492. }
  493. if (target) {
  494. handleWarningMode.value = 'reportOnly'
  495. currentWarningElderly.value = target
  496. handleWarningDialogVisible.value = true
  497. } else {
  498. handleWarningMode.value = 'reportOnly'
  499. currentWarningElderly.value = null
  500. handleWarningDialogVisible.value = true
  501. // ElMessage.warning('未找到对应长者,将以上报方式处理')
  502. }
  503. }
  504. // API 调用
  505. const getStatistics = async () => {
  506. const res = await fetchHttp.get(
  507. '/api/pc/admin/getStatistics?organizationId=' + organizationId,
  508. {},
  509. {
  510. headers: {
  511. Authorization: `Bearer ${getAccessToken()}`
  512. }
  513. }
  514. )
  515. if (res.list && res.list.length) {
  516. res.list.forEach((v: any) => {
  517. const index = largeScreenStats.value.findIndex((z) => z.indicator == v.indicator)
  518. if (~index) {
  519. largeScreenStats.value[index].value = v.total
  520. largeScreenStats.value[index].change = v.change
  521. largeScreenStats.value[index].trend = v.trend
  522. }
  523. })
  524. }
  525. }
  526. const getElderList = async () => {
  527. const res = await fetchHttp.get(
  528. '/api/pc/admin/getElderList?organizationId=' + organizationId,
  529. {},
  530. {
  531. headers: {
  532. Authorization: `Bearer ${getAccessToken()}`
  533. }
  534. }
  535. )
  536. if (res?.length) {
  537. elderlyList.value = res
  538. if (elderlyList.value.length) {
  539. selectedElderly.value.id = elderlyList.value[0].id
  540. selectedElderly.value.name = elderlyList.value[0].name
  541. getElderDeviceMessage(selectedElderly.value.id)
  542. warningFlags.value = []
  543. }
  544. }
  545. }
  546. const getDeviceDetail = async (deviceCode: string) => {
  547. const res = await fetchHttp.get(
  548. '/api/pc/admin/getDeviceDetail?deviceCode=' + deviceCode,
  549. {},
  550. {
  551. headers: {
  552. Authorization: `Bearer ${getAccessToken()}`
  553. }
  554. }
  555. )
  556. if (res) {
  557. deviceDetail.value = res
  558. }
  559. }
  560. const getElderDeviceMessage = async (elderId: number) => {
  561. const res = await fetchHttp.get(
  562. '/api/pc/admin/getElderHealthAndDeviceInfo?elderId=' + elderId,
  563. {},
  564. {
  565. headers: {
  566. Authorization: `Bearer ${getAccessToken()}`
  567. }
  568. }
  569. )
  570. if (res) {
  571. selectedElderly.value.healthList = res.healthList
  572. selectedElderly.value.deviceList = res.deviceList
  573. }
  574. }
  575. const getAllWarning = async () => {
  576. const params = {
  577. pageNum: pageNum.value,
  578. pageSize: pageSize.value
  579. }
  580. const res = await fetchHttp.get('/api/pc/admin/getAllWarning', params, {
  581. headers: {
  582. Authorization: `Bearer ${getAccessToken()}`
  583. }
  584. })
  585. if (res) {
  586. total.value = res.total
  587. warningData.value = res.list || []
  588. }
  589. }
  590. const getAllDevices = async () => {
  591. const res = await fetchHttp.get(
  592. '/api/pc/admin/getAllDeviceTypes',
  593. {},
  594. {
  595. headers: {
  596. Authorization: `Bearer ${getAccessToken()}`
  597. }
  598. }
  599. )
  600. if (res && res?.length) {
  601. deviceTypeOptions.value = res
  602. }
  603. }
  604. // WebSocket 处理
  605. const addFlashingEffect = (elder: Elderly) => {
  606. elder._flashEffect = true
  607. setTimeout(() => {
  608. elder._flashEffect = false
  609. elderlyList.value = [...elderlyList.value]
  610. }, 10000)
  611. }
  612. const handelDeviceRefresh = async () => {
  613. await refreshAllDevices()
  614. ElMessage.success('设备刷新成功!')
  615. }
  616. // 全部设备视图相关方法
  617. const refreshAllDevices = async () => {
  618. try {
  619. const res = await fetchHttp.get(
  620. '/api/pc/admin/getOrganizationDevices',
  621. {},
  622. {
  623. headers: {
  624. Authorization: `Bearer ${getAccessToken()}`
  625. }
  626. }
  627. )
  628. console.log('res', res)
  629. if (res?.deviceList && Array.isArray(res.deviceList)) {
  630. allDevices.value = res.deviceList
  631. }
  632. } catch (e) {
  633. console.error('获取全部设备失败:', e)
  634. allDevices.value = []
  635. }
  636. }
  637. const openAllDevicesView = async () => {
  638. showAllDevices.value = true
  639. await refreshAllDevices()
  640. }
  641. const backToOverview = () => {
  642. showAllDevices.value = false
  643. }
  644. const showDeviceDetailByCode = async (deviceCode: string) => {
  645. await getDeviceDetail(deviceCode)
  646. deviceDetailVisible.value = true
  647. }
  648. const handleSOSAlert = async (alertData: any) => {
  649. const alert = alertData.data || alertData
  650. sendMessage({
  651. type: 'SOS_ACK',
  652. alertId: alertData.timestamp,
  653. timestamp: Date.now()
  654. })
  655. // 获取地址信息
  656. let addressInfo = '位置信息获取中...'
  657. if (alert.longitude && alert.latitude) {
  658. addressInfo = await amapReverseGeocode(alert.longitude, alert.latitude)
  659. }
  660. ElNotification({
  661. title: '🚨🚨 SOS紧急预警 🚨🚨',
  662. customClass: 'my-warning-notification',
  663. message: h('div', {
  664. style: { color: '#fff' },
  665. innerHTML: `
  666. <div>院区名称: ${alert.organizationName || '未知'}</div>
  667. <div>长者姓名: ${alert.elderName || '未知'}</div>
  668. <div>长者房间: ${alert.roomName || '未知'}</div>
  669. <div>设备类型: ${alert.deviceType || '未知'}</div>
  670. <div>设备电量: ${alert.batteryLevel || '0%'}</div>
  671. <div>位置信息: ${addressInfo}</div>
  672. <div>时间: ${new Date(alertData.timestamp).toLocaleString()}</div>
  673. `
  674. }),
  675. type: 'warning',
  676. duration: 10000
  677. })
  678. if (alert.elderId) addWarningFlag(alert.elderId)
  679. if (alert.elderId && elderlyList.value.length > 0) {
  680. const elderIndex = elderlyList.value.findIndex((elder) => elder.id === alert.elderId)
  681. if (elderIndex !== -1) {
  682. const reorderedList = [...elderlyList.value]
  683. const [selectedElder] = reorderedList.splice(elderIndex, 1)
  684. reorderedList.unshift(selectedElder)
  685. elderlyList.value = reorderedList
  686. addFlashingEffect(selectedElder)
  687. getElderDeviceMessage(alert.elderId)
  688. }
  689. }
  690. largeScreenStatsData.value = {
  691. systemStatus: '告警',
  692. lastTime: new Date(alertData.timestamp).toLocaleString(),
  693. isWarning: true
  694. }
  695. setTimeout(() => {
  696. largeScreenStatsData.value = {
  697. systemStatus: '正常',
  698. lastTime: new Date().toLocaleString(),
  699. isWarning: false
  700. }
  701. }, 10000)
  702. }
  703. const handleHealthAlert = async (healthAlert: any) => {
  704. const healthAlertData = healthAlert.data
  705. if (healthAlertData.elderId) addWarningFlag(healthAlertData.elderId)
  706. // 获取地址信息
  707. let addressInfo = ''
  708. if (healthAlertData.longitude && healthAlertData.latitude) {
  709. addressInfo = await amapReverseGeocode(healthAlertData.longitude, healthAlertData.latitude)
  710. }
  711. ElNotification({
  712. title: `🚨🚨 ${healthAlertData.eventType} 🚨🚨`,
  713. customClass: 'my-warning-notification',
  714. message: h('div', {
  715. style: { color: '#fff' },
  716. innerHTML: `
  717. <div>长者姓名: ${healthAlertData.elderName || '未知'}</div>
  718. <div>预警指标: ${healthAlertData.indicatorName || '未知'}</div>
  719. <div>预警信息: ${healthAlertData.message || '未知'}</div>
  720. <div>处理建议: ${healthAlertData.suggestion || '未知'}</div>
  721. <div>预警程度: ${healthAlertData.alertLevel || ''}</div>
  722. <div>位置信息: ${addressInfo}</div>
  723. <div>时间: ${new Date(healthAlertData.timestamp).toLocaleString()}</div>
  724. `
  725. }),
  726. type: 'warning',
  727. duration: 10000
  728. })
  729. if (healthAlertData.elderId && elderlyList.value.length > 0) {
  730. const elderIndex = elderlyList.value.findIndex((elder) => elder.id === healthAlertData.elderId)
  731. if (elderIndex !== -1) {
  732. elderlyList.value[elderIndex].healthText =
  733. healthAlertData.message + healthAlertData.suggestion
  734. const reorderedList = [...elderlyList.value]
  735. const [selectedElder] = reorderedList.splice(elderIndex, 1)
  736. reorderedList.unshift(selectedElder)
  737. elderlyList.value = reorderedList
  738. addFlashingEffect(selectedElder)
  739. getElderDeviceMessage(healthAlertData.elderId)
  740. }
  741. }
  742. largeScreenStatsData.value = {
  743. systemStatus: '健康告警',
  744. lastTime: new Date(healthAlertData.timestamp).toLocaleString(),
  745. isWarning: true
  746. }
  747. setTimeout(() => {
  748. largeScreenStatsData.value = {
  749. systemStatus: '设备正常',
  750. lastTime: new Date().toLocaleString(),
  751. isWarning: false
  752. }
  753. }, 10000)
  754. }
  755. const handleLocationAlert = async (locationAlert: any) => {
  756. try {
  757. const payload = locationAlert?.data || {}
  758. const elderId = Number(payload.elderId || 0)
  759. const lng = Number(payload.longitude)
  760. const lat = Number(payload.latitude)
  761. const ts = locationAlert.timestamp
  762. // 推入右上角实时抽屉
  763. realtimeDrawerRef.value?.pushLocation?.({
  764. longitude: lng,
  765. latitude: lat,
  766. locationTime: ts,
  767. elderName: payload.elderName
  768. })
  769. // 地图联动:只对当前选中长者联动地图
  770. if (!elderId || elderId !== selectedElderly.value.id) return
  771. if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
  772. const isMapShown = detailSectionRef.value?.isMapShown?.()
  773. if (!isMapShown) return
  774. detailSectionRef.value?.addMapPoint?.({
  775. longitude: lng,
  776. latitude: lat,
  777. locationTime: new Date(ts).toLocaleString()
  778. })
  779. } catch (e) {
  780. console.warn('处理 LOCATION_UPDATE 失败(忽略):', e)
  781. }
  782. }
  783. const handleHealthUpdateAlert = async (healthUpdateAlert: any) => {
  784. realtimeDrawerRef.value?.pushHealth?.({
  785. time: healthUpdateAlert.timestamp,
  786. elderName: healthUpdateAlert.data.elderName,
  787. indicatorResults: healthUpdateAlert.data.realtimeHealthAnalysisResult.indicatorResults
  788. })
  789. }
  790. const handleDeviceDataUpdate = async (deviceDataUpdate: any) => {
  791. ElNotification({
  792. title: `设备状态更新`,
  793. customClass: 'my-warning-notification',
  794. message: h('div', {
  795. style: { color: '#fff' },
  796. innerHTML: `
  797. <div>设备名称: ${deviceDataUpdate.data.deviceType || '未知'}</div>
  798. <div>设备状态: ${deviceDataUpdate.data.eventType || '未知'}</div>
  799. <div>绑定长者: ${deviceDataUpdate.data.elderName || '未知'}</div>
  800. <div>时间: ${new Date(deviceDataUpdate.timestamp).toLocaleString()}</div>
  801. `
  802. }),
  803. type: 'warning',
  804. duration: 10000
  805. })
  806. realtimeDrawerRef.value?.pushDeviceUpdate?.({
  807. time: deviceDataUpdate.timestamp,
  808. elderName: deviceDataUpdate.data.elderName,
  809. eventType: deviceDataUpdate.data.eventType,
  810. deviceType: deviceDataUpdate.data.deviceType
  811. })
  812. // 判断deviceDataUpdate.data.elderId是否跟当前点击的老人id一致,如果一致,则更新当前点击的老人设备列表
  813. if (deviceDataUpdate.data.elderId === selectedElderly.value.id) {
  814. getElderDeviceMessage(selectedElderly.value.id)
  815. }
  816. }
  817. // WebSocket 连接
  818. const wsUrl = import.meta.env.VITE_API_WSS_URL_MONITOR
  819. const { connect, disconnect, sendMessage } = useWebSocket({
  820. wsUrl,
  821. onSOSAlert: handleSOSAlert,
  822. onHealthAlert: handleHealthAlert,
  823. onLocationAlert: handleLocationAlert,
  824. onHealthUpdateAlert: handleHealthUpdateAlert,
  825. onDeviceDataUpdate: handleDeviceDataUpdate
  826. })
  827. // 生命周期
  828. onMounted(() => {
  829. loadWarningFlags()
  830. getAllDevices()
  831. getStatistics()
  832. getElderList()
  833. setTimeout(() => {
  834. connect()
  835. }, 1000)
  836. document.addEventListener('visibilitychange', () => {
  837. if (!document.hidden) {
  838. // 页面回到前台
  839. }
  840. })
  841. setInterval(() => {
  842. // 定期健康检查
  843. }, 30000)
  844. window.addEventListener('online', () => {
  845. // 网络恢复
  846. })
  847. })
  848. onUnmounted(() => {
  849. disconnect()
  850. })
  851. </script>
  852. <style lang="scss" scoped>
  853. $primary-color: #1a73e8;
  854. $secondary-color: #00c6ff;
  855. $accent-color: #7b61ff;
  856. $dark-bg: #0a0e14;
  857. $card-bg: #1a1f2e;
  858. $text-light: #fff;
  859. $text-gray: #8a8f98;
  860. $success-color: #26de81;
  861. $warning-color: #fd9644;
  862. $danger-color: #ff6b6b;
  863. $bg-gradient-start: #04060f;
  864. $bg-gradient-mid: #0c1631;
  865. $bg-gradient-end: #101b3f;
  866. $bg-accent-1: rgb(32 156 255 / 35%);
  867. $bg-accent-2: rgb(123 97 255 / 25%);
  868. @keyframes auroraShift {
  869. 0% {
  870. opacity: 0.6;
  871. transform: translate(-10%, -10%) scale(1);
  872. }
  873. 50% {
  874. opacity: 0.9;
  875. transform: translate(5%, 10%) scale(1.1);
  876. }
  877. 100% {
  878. opacity: 0.6;
  879. transform: translate(15%, -5%) scale(1.05);
  880. }
  881. }
  882. .large-screen {
  883. position: relative;
  884. padding: 20px;
  885. overflow: hidden;
  886. height: calc(
  887. 100vh - var(--app-content-padding) - var(--app-content-padding) - var(--app-footer-height)
  888. );
  889. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
  890. sans-serif;
  891. color: $text-light;
  892. background: radial-gradient(circle at 10% 20%, rgb(15 88 255 / 25%) 0%, transparent 35%),
  893. radial-gradient(circle at 90% 10%, rgb(20 201 201 / 18%) 0%, transparent 30%),
  894. linear-gradient(135deg, $bg-gradient-start 0%, $bg-gradient-mid 45%, $bg-gradient-end 100%);
  895. &::before,
  896. &::after {
  897. position: absolute;
  898. pointer-events: none;
  899. background: radial-gradient(circle, $bg-accent-1 0%, transparent 60%);
  900. content: '';
  901. opacity: 0.7;
  902. filter: blur(80px);
  903. animation: auroraShift 18s ease-in-out infinite alternate;
  904. inset: -30%;
  905. }
  906. &::after {
  907. background: radial-gradient(circle, $bg-accent-2 0%, transparent 60%);
  908. animation-delay: 4s;
  909. animation-duration: 22s;
  910. }
  911. .my-container {
  912. display: flex;
  913. height: 100%;
  914. flex-direction: column;
  915. }
  916. }
  917. .cyber-bg {
  918. position: fixed;
  919. top: 0;
  920. left: 0;
  921. z-index: -2;
  922. width: 100%;
  923. height: 100%;
  924. background: radial-gradient(circle at 20% 30%, rgb(34 123 255 / 18%) 0%, transparent 45%),
  925. radial-gradient(circle at 80% 70%, rgb(123 97 255 / 15%) 0%, transparent 50%),
  926. radial-gradient(circle at 50% 50%, rgb(0 255 240 / 8%) 0%, transparent 55%);
  927. opacity: 0.5;
  928. filter: blur(10px);
  929. }
  930. .cyber-grid {
  931. position: fixed;
  932. top: 0;
  933. left: 0;
  934. z-index: -1;
  935. width: 100%;
  936. height: 100%;
  937. background-image: linear-gradient(rgb(255 255 255 / 5%) 1px, transparent 1px),
  938. linear-gradient(90deg, rgb(255 255 255 / 5%) 1px, transparent 1px);
  939. background-position: center;
  940. background-size: 40px 40px;
  941. opacity: 0.35;
  942. mix-blend-mode: screen;
  943. }
  944. .main-content-large {
  945. display: grid;
  946. min-height: 0;
  947. margin-bottom: 15px;
  948. flex: 1;
  949. grid-template-columns: 1fr 2fr;
  950. gap: 20px;
  951. }
  952. :deep(.el-input__wrapper) {
  953. padding: 15px 20px !important;
  954. font-size: 16px !important;
  955. background: rgb(255 255 255 / 10%) !important;
  956. border-radius: 10px !important;
  957. }
  958. :deep(.el-input__inner) {
  959. font-size: 16px !important;
  960. color: $text-light !important;
  961. }
  962. :deep(.el-button) {
  963. display: flex !important;
  964. padding: 12px 24px !important;
  965. font-size: 16px !important;
  966. font-weight: 500 !important;
  967. border-radius: 10px !important;
  968. transition: all 0.3s !important;
  969. align-items: center !important;
  970. gap: 8px !important;
  971. }
  972. :deep(.el-button--primary) {
  973. background: linear-gradient(90deg, $primary-color, $accent-color) !important;
  974. border: none !important;
  975. box-shadow: 0 4px 15px rgb(26 115 232 / 30%) !important;
  976. }
  977. :deep(.el-button--primary:hover) {
  978. transform: translateY(-2px) !important;
  979. box-shadow: 0 8px 25px rgb(26 115 232 / 40%) !important;
  980. }
  981. /* 全部设备视图样式 */
  982. .all-devices-view {
  983. padding: 10px 0 20px;
  984. .all-devices-header {
  985. display: flex;
  986. align-items: center;
  987. justify-content: space-between;
  988. margin-bottom: 12px;
  989. h3 {
  990. margin: 0;
  991. font-size: 18px;
  992. }
  993. .actions {
  994. display: flex;
  995. gap: 10px;
  996. }
  997. }
  998. .device-grid {
  999. display: grid;
  1000. grid-template-columns: repeat(3, 1fr);
  1001. gap: 16px;
  1002. }
  1003. .device-card {
  1004. background: rgb(255 255 255 / 6%);
  1005. border: 1px solid rgb(255 255 255 / 12%);
  1006. border-radius: 12px;
  1007. padding: 14px;
  1008. display: flex;
  1009. flex-direction: column;
  1010. gap: 10px;
  1011. .device-card-header {
  1012. display: flex;
  1013. align-items: center;
  1014. justify-content: space-between;
  1015. .device-type {
  1016. font-weight: 600;
  1017. }
  1018. .device-code {
  1019. color: $text-gray;
  1020. font-size: 12px;
  1021. }
  1022. }
  1023. .device-meta {
  1024. display: grid;
  1025. grid-template-columns: 1fr;
  1026. gap: 4px;
  1027. color: $text-gray;
  1028. font-size: 14px;
  1029. }
  1030. .device-history {
  1031. background: rgb(255 255 255 / 4%);
  1032. border: 1px solid rgb(255 255 255 / 10%);
  1033. border-radius: 8px;
  1034. padding: 8px;
  1035. .history-title {
  1036. font-size: 13px;
  1037. color: #cfd3dc;
  1038. margin-bottom: 6px;
  1039. }
  1040. .history-list {
  1041. list-style: none;
  1042. padding: 0;
  1043. margin: 0;
  1044. display: flex;
  1045. flex-direction: column;
  1046. gap: 6px;
  1047. li {
  1048. display: grid;
  1049. grid-template-columns: 150px 1fr;
  1050. gap: 8px;
  1051. font-size: 12px;
  1052. .time {
  1053. color: #a5b1c2;
  1054. }
  1055. .content {
  1056. color: #ffffff;
  1057. }
  1058. }
  1059. }
  1060. }
  1061. .device-actions {
  1062. display: flex;
  1063. justify-content: flex-end;
  1064. }
  1065. }
  1066. }
  1067. /* 右上角悬浮按钮 */
  1068. .realtime-fab {
  1069. position: fixed;
  1070. top: 230px;
  1071. right: 16px;
  1072. width: 80px;
  1073. height: 80px;
  1074. display: flex;
  1075. align-items: center;
  1076. justify-content: center;
  1077. cursor: pointer;
  1078. border: none;
  1079. border-radius: 50%;
  1080. color: #fff;
  1081. background: var(--el-color-warning);
  1082. box-shadow: 0 6px 18px rgb(0 0 0 / 35%);
  1083. backdrop-filter: blur(6px);
  1084. z-index: 999;
  1085. transition: all 0.2s ease;
  1086. }
  1087. .realtime-fab:hover {
  1088. transform: translateY(-2px);
  1089. box-shadow: 0 10px 26px rgb(0 0 0 / 45%);
  1090. background: #ffb74d;
  1091. }
  1092. .realtime-fab:active {
  1093. transform: translateY(0);
  1094. }
  1095. // .realtime-fab :deep(svg),
  1096. // .realtime-fab :deep(span) {
  1097. // width: 22px !important;
  1098. // height: 22px !important;
  1099. // }
  1100. </style>
  1101. <style lang="scss">
  1102. /* 滚动条优化 */
  1103. .elderly-scroll-container::-webkit-scrollbar,
  1104. .detail-section::-webkit-scrollbar,
  1105. .history-list::-webkit-scrollbar {
  1106. width: 8px;
  1107. }
  1108. .elderly-scroll-container::-webkit-scrollbar-track,
  1109. .detail-section::-webkit-scrollbar-track,
  1110. .history-list::-webkit-scrollbar-track {
  1111. background: rgb(255 255 255 / 5%);
  1112. border-radius: 4px;
  1113. }
  1114. .elderly-scroll-container::-webkit-scrollbar-thumb,
  1115. .detail-section::-webkit-scrollbar-thumb,
  1116. .history-list::-webkit-scrollbar-thumb {
  1117. background: rgb(26 115 232 / 50%);
  1118. border-radius: 4px;
  1119. }
  1120. .large-screen-dialog {
  1121. margin-top: 8vh !important;
  1122. background: #1a1f2e !important;
  1123. border: 1px solid rgb(255 255 255 / 10%) !important;
  1124. border-radius: 16px !important;
  1125. box-shadow: 0 20px 60px rgb(0 0 0 / 40%) !important;
  1126. .el-dialog__header {
  1127. padding: 25px !important;
  1128. margin-top: 20px;
  1129. color: white !important;
  1130. background: linear-gradient(90deg, var(--el-color-primary), #7b61ff) !important;
  1131. border-radius: 16px 16px 0 0 !important;
  1132. }
  1133. .el-dialog__title {
  1134. font-size: 20px !important;
  1135. font-weight: 600 !important;
  1136. color: white !important;
  1137. }
  1138. .el-dialog__body {
  1139. padding: 30px !important;
  1140. color: #fff !important;
  1141. }
  1142. }
  1143. .my-warning-notification {
  1144. color: #fff !important;
  1145. background: linear-gradient(135deg, #1e4184 0%, #3469e3 100%) !important;
  1146. border: 1px solid rgb(42 157 143 / 30%) !important;
  1147. box-shadow: 0 4px 20px rgb(0 0 0 / 50%) !important;
  1148. .el-notification__group {
  1149. .el-notification__title {
  1150. color: #fff !important;
  1151. }
  1152. }
  1153. }
  1154. </style>