index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <template>
  2. <div class="bed-card-container">
  3. <!-- 床头卡主体 -->
  4. <div class="bed-card">
  5. <!-- 头部信息 -->
  6. <div class="card-header">
  7. <img src="../../assets/imgs/logo_card.png" alt="颐年医养" class="header-logo" />
  8. <div style="display: flex; align-items: center">
  9. <h2 class="header-title">长者床头卡</h2>
  10. </div>
  11. <div style="font-size: 16px; color: #fff; margin-right: 10px"
  12. >更新时间:{{ formatToDate(patientData.updateTime) || '' }}</div
  13. >
  14. <!-- <div class="header-room">{{ patientData.roomName || '' }}</div>-->
  15. </div>
  16. <div class="card-main">
  17. <!-- 左侧床号信息 -->
  18. <div class="left-section">
  19. <div class="bed-info">
  20. <div class="bed-letter" v-for="(item, index) in bedList" :key="index">
  21. <span>{{ item || '' }}</span>
  22. <span v-if="index == (bedList || []).length - 1 && !item.includes('床')" class="bed-text"
  23. >床</span
  24. >
  25. </div>
  26. </div>
  27. <!-- 二维码 -->
  28. <div class="qrcode-container">
  29. <Qrcode :text="qrCodeContent" :width="120" :logo="logoUrl" tag="canvas" />
  30. <div class="qrcode-text">长者二维码</div>
  31. </div>
  32. </div>
  33. <!-- 右侧信息表格 -->
  34. <div class="right-section">
  35. <table class="info-table">
  36. <!-- 基本信息行 -->
  37. <tr class="row-basic">
  38. <td class="table-label">姓名</td>
  39. <td class="table-data">{{ patientData.elderName || '--' }}</td>
  40. <td class="table-label">性别</td>
  41. <td class="table-data">{{ patientData.elderSex || '--' }}</td>
  42. <td class="table-label">生日</td>
  43. <td class="table-data">{{ patientData.birthday || '--' }}</td>
  44. <td class="table-label">入住时间</td>
  45. <td class="table-data">{{ patientData.checkInTime || '--' }}</td>
  46. </tr>
  47. <!-- 护理等级 / 照护要点 / 过敏史 -->
  48. <tr class="row-care">
  49. <td class="table-label">护理等级</td>
  50. <td class="table-data">{{ patientData.careLevel || '--' }}</td>
  51. <td class="table-label">照护要点</td>
  52. <td class="table-data" style="font-size: 14px" colspan="3">{{
  53. patientData.carePoints || '--'
  54. }}</td>
  55. <td class="table-label">过敏史</td>
  56. <td class="table-data" style="font-size: 14px">{{
  57. patientData.allergiesText || '--'
  58. }}</td>
  59. </tr>
  60. <!-- 既往病史 / 用药史 -->
  61. <tr class="row-medical">
  62. <td class="table-label">既往病史</td>
  63. <td class="table-data" style="font-size: 14px" colspan="3">{{
  64. patientData.medicalHistory || '--'
  65. }}</td>
  66. <td class="table-label">用药史</td>
  67. <td class="table-data" style="font-size: 14px" colspan="3">{{
  68. patientData.medicationHistory || '--'
  69. }}</td>
  70. </tr>
  71. <!-- 饮食要点 -->
  72. <tr class="row-diet">
  73. <td class="table-label">饮食要点</td>
  74. <td class="table-data diet-cell" colspan="7">
  75. <div v-if="hasDietGuidelines" class="diet-list">
  76. <div
  77. v-for="item in dietTypes"
  78. :key="item.value"
  79. class="diet-item"
  80. :class="{ active: isDietActive(item.label) }"
  81. >
  82. <el-icon v-if="isDietActive(item.label)" style="margin-right: 2px" size="22"
  83. ><Check
  84. /></el-icon>
  85. <!-- <span v-if="isDietActive(item.label)" class="diet-check">√</span>-->
  86. <span class="diet-name" style="font-size: 16px">{{ item.label }}</span>
  87. </div>
  88. </div>
  89. <div v-else class="diet-empty">--</div>
  90. </td>
  91. </tr>
  92. <!-- 九防注意事项 -->
  93. <tr class="row-prevention">
  94. <td class="table-label prevention-title">
  95. <div class="vertical-text">九防注意事项</div>
  96. </td>
  97. <td class="table-data prevention-cell" colspan="7">
  98. <div class="prevention-grid">
  99. <div
  100. v-for="(item, index) in preventionItems"
  101. :key="item.key"
  102. class="prevention-item"
  103. :class="[`prevention-pos-${index + 1}`]"
  104. >
  105. <!-- <div class="risk-badge" v-if="patientData[item.field]">
  106. <span class="risk-star">★</span>
  107. <span class="risk-text">{{ getRiskText(patientData[item.field]) }}</span>
  108. </div> -->
  109. <img
  110. v-if="
  111. patientData[item.field] &&
  112. getPreventionImage(item.field, patientData[item.field])
  113. "
  114. :src="getPreventionImage(item.field, patientData[item.field])"
  115. :alt="item.name"
  116. class="placeholder-image"
  117. />
  118. <div
  119. v-else
  120. class="prevention-image placeholder-image"
  121. style="display: flex; flex-direction: column"
  122. >
  123. <div style="font-size: 14px; color: #666666">{{ item.name }}</div>
  124. <div style="font-size: 14px; color: #666666">(无风险)</div>
  125. </div>
  126. <!-- <div class="prevention-name">{{ item.name }}</div>-->
  127. </div>
  128. </div>
  129. </td>
  130. </tr>
  131. <!-- 责任人员 -->
  132. <tr class="row-staff">
  133. <td class="table-label"><div>责任</div><div>护理员</div></td>
  134. <td class="table-data" colspan="2">{{
  135. patientData.responsibleStaff?.nurseAide || '--'
  136. }}</td>
  137. <td class="table-label">责任护士</td>
  138. <td class="table-data" colspan="2">{{
  139. patientData.responsibleStaff?.nurse || '--'
  140. }}</td>
  141. <td class="table-label">主管医生</td>
  142. <td class="table-data">{{ patientData.responsibleStaff?.doctor || '--' }}</td>
  143. </tr>
  144. </table>
  145. </div>
  146. </div>
  147. <!-- 底部装饰条 -->
  148. <div class="card-footer"></div>
  149. </div>
  150. </div>
  151. </template>
  152. <script setup lang="ts">
  153. import { ref, reactive, onMounted, computed } from 'vue'
  154. // 定义全局window接口扩展
  155. declare global {
  156. interface Window {
  157. __GlobalZipManager?: any
  158. }
  159. }
  160. import { ElMessage } from 'element-plus'
  161. import html2canvas from 'html2canvas'
  162. import { Qrcode } from '@/components/Qrcode'
  163. import JSZip from 'jszip'
  164. import { Check } from '@element-plus/icons-vue'
  165. import { formatToDate } from '@/utils/dateUtil'
  166. // Props定义
  167. const props = defineProps<{
  168. roomNumber?: string
  169. bedLetter?: string
  170. showLogo?: boolean
  171. qrcodeUrl?: string
  172. }>()
  173. // 默认值
  174. const qrCodeContent = ref('')
  175. const logoUrl = ref('/logo.png')
  176. // 饮食类型
  177. const dietTypes = [
  178. { value: '1', label: '普食' },
  179. { value: '2', label: '软食' },
  180. { value: '3', label: '半流质' },
  181. { value: '4', label: '流质' },
  182. { value: '5', label: '鼻饲' }
  183. ]
  184. // 九防项目配置
  185. const preventionItems = [
  186. { key: 'asphyxiation', name: '防噎食', field: 'asphyxiationByChoking' },
  187. { key: 'pressureSores', name: '防压疮', field: 'pressureSores' },
  188. { key: 'bedFall', name: '防坠床', field: 'fallPreventionMeasures' },
  189. { key: 'selfHarm', name: '防他伤和自伤', field: 'commitSuicide' },
  190. { key: 'foodDrug', name: '防误食', field: 'assessSimpleMentalState' },
  191. { key: 'scald', name: '防烫伤', field: 'empyrosis' },
  192. { key: 'fall', name: '防跌倒', field: 'fallDown' },
  193. { key: 'wandering', name: '防走失', field: 'wanderAway' },
  194. { key: 'entertainment', name: '防文娱活动意外', field: 'antiEntertainment' }
  195. ]
  196. // 判断饮食类型是否激活
  197. const hasDietGuidelines = computed(() => {
  198. return !!(patientData.dietaryGuidelines && patientData.dietaryGuidelines.trim())
  199. })
  200. const isDietActive = (label: string) => {
  201. const value = patientData.dietaryGuidelines
  202. // 1. 如果是空值,直接返回 false
  203. if (!value) return false
  204. // 2. 如果是字符串(例如 '半流质'),直接做全等判断
  205. if (typeof value === 'string') {
  206. return value === label
  207. }
  208. return false
  209. }
  210. // 风险等级文本
  211. const getRiskText = (value: string) => {
  212. const map: Record<string, string> = {
  213. high: '高风险',
  214. medium: '中风险',
  215. low: '低风险',
  216. none: '无风险'
  217. }
  218. return map[value] || value || ''
  219. }
  220. // 根据风险等级获取九防图片地址
  221. const getPreventionImage = (field: string, level: string) => {
  222. const levelMap: Record<string, string> = {
  223. 低: '01',
  224. 中: '02',
  225. 高: '03'
  226. }
  227. const suffix = levelMap[level] || '00'
  228. if (suffix === '00') {
  229. return false
  230. } else {
  231. return `/jiufang/${field}-${suffix}.png`
  232. }
  233. }
  234. // 患者数据
  235. const patientData = reactive({
  236. roomName: '',
  237. bedName: '',
  238. elderName: '',
  239. elderSex: '',
  240. birthday: '',
  241. checkInTime: '',
  242. careLevel: '',
  243. carePoints: '',
  244. allergiesText: '',
  245. medicalHistory: '',
  246. medicationHistory: '',
  247. dietaryGuidelines: '',
  248. responsibleStaff: {
  249. nurseAide: '',
  250. nurse: '',
  251. doctor: ''
  252. },
  253. asphyxiationByChoking: '',
  254. pressureSores: '',
  255. fallPreventionMeasures: '',
  256. assessSimpleMentalState: '',
  257. empyrosis: '',
  258. fallDown: '',
  259. wanderAway: '',
  260. antiEntertainment: '',
  261. commitSuicide: '',
  262. qr: {
  263. eId: '',
  264. tId: ''
  265. }
  266. })
  267. // 使用全局变量方式实现真正的全局单例管理
  268. if (!(window as any).__GlobalZipManager) {
  269. ;(window as any).__GlobalZipManager = {
  270. instance: null as JSZip | null,
  271. count: 0,
  272. getZip: function (): JSZip {
  273. if (!this.instance) {
  274. this.instance = new JSZip()
  275. this.count = 0
  276. console.log('创建了新的全局JSZip单例实例')
  277. }
  278. return this.instance
  279. },
  280. incrementCount: function (): void {
  281. this.count++
  282. },
  283. getCount: function (): number {
  284. return this.count
  285. },
  286. reset: function (): void {
  287. this.instance = null
  288. this.count = 0
  289. console.log('已重置全局JSZip单例实例')
  290. },
  291. getFileList: function (): string[] {
  292. if (!this.instance) return []
  293. return Object.keys(this.instance.files)
  294. }
  295. }
  296. }
  297. const GlobalZipManager = (window as any).__GlobalZipManager
  298. const fileCount = ref(0)
  299. onMounted(() => {})
  300. // 导出床头卡
  301. const exportAsImage = async (type: number) => {
  302. try {
  303. const element = document.querySelector('.bed-card')
  304. if (!element) return
  305. const canvas = await html2canvas(element as HTMLElement, {
  306. scale: 2,
  307. useCORS: true,
  308. backgroundColor: '#ffffff',
  309. logging: false,
  310. allowTaint: true,
  311. foreignObjectRendering: false,
  312. imageTimeout: 0,
  313. removeContainer: true,
  314. scrollX: 0,
  315. scrollY: 0,
  316. width: element.scrollWidth,
  317. height: element.scrollHeight
  318. })
  319. const fileName = `${patientData.elderName}${patientData.bedName}床头卡.png`
  320. const imageBase64 = canvas.toDataURL('image/png', 1.0)
  321. const base64Data = imageBase64.split(',')[1]
  322. if (type === 2) {
  323. const zipInstance = GlobalZipManager.getZip()
  324. zipInstance.file(fileName, base64Data, { base64: true })
  325. GlobalZipManager.incrementCount()
  326. fileCount.value = GlobalZipManager.getCount()
  327. } else if (type === 3) {
  328. const zipInstance = GlobalZipManager.getZip()
  329. zipInstance.file(fileName, base64Data, { base64: true })
  330. GlobalZipManager.incrementCount()
  331. fileCount.value = GlobalZipManager.getCount()
  332. const fileNames = GlobalZipManager.getFileList()
  333. const content = await zipInstance.generateAsync({ type: 'blob' })
  334. const link = document.createElement('a')
  335. link.href = URL.createObjectURL(content)
  336. link.download = `床头卡压缩包_${new Date().getTime()}.zip`
  337. link.click()
  338. GlobalZipManager.reset()
  339. fileCount.value = 0
  340. ElMessage.success(`床头卡压缩包导出成功,共包含${fileNames.length}个文件`)
  341. } else {
  342. const link = document.createElement('a')
  343. link.href = imageBase64
  344. link.download = fileName
  345. link.click()
  346. ElMessage.success('床头卡导出成功')
  347. }
  348. } catch (error) {
  349. console.error('导出失败:', error)
  350. ElMessage.error('导出失败,请稍后重试')
  351. if (type === 2 || type === 3) {
  352. GlobalZipManager.reset()
  353. fileCount.value = 0
  354. }
  355. }
  356. }
  357. const bedList = ref<string[]>()
  358. // 更新患者数据的方法
  359. const updatePatientData = (data: any) => {
  360. qrCodeContent.value =
  361. 'https://callback.ynims.com/qrcode/elderlyId?=' + data.qr?.eId + '&tenantId=' + data.qr?.tId
  362. Object.assign(patientData, data)
  363. console.log('收到数据:', data)
  364. try {
  365. bedList.value = patientData.bedName.toString().split('-')
  366. //console.log(bedList.value)
  367. } catch (e) {}
  368. }
  369. // 重置临时zip数据
  370. const resetTempZip = () => {
  371. GlobalZipManager.reset()
  372. fileCount.value = 0
  373. console.log('已重置临时zip数据')
  374. }
  375. // 暴露方法给父组件
  376. defineExpose({
  377. updatePatientData,
  378. exportAsImage,
  379. resetTempZip
  380. })
  381. </script>
  382. <style scoped>
  383. .bed-card-container {
  384. display: flex;
  385. flex-direction: column;
  386. align-items: center;
  387. padding: 12px;
  388. }
  389. .bed-card {
  390. width: 980px;
  391. height: 680px;
  392. background-color: white;
  393. border: 1px solid #e0e0e0;
  394. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  395. position: relative;
  396. overflow: hidden;
  397. display: flex;
  398. flex-direction: column;
  399. }
  400. .card-header {
  401. background-color: #e78d35;
  402. color: white;
  403. padding: 8px 10px;
  404. display: flex;
  405. align-items: center;
  406. justify-content: space-between;
  407. height: 76px;
  408. box-sizing: border-box;
  409. }
  410. .header-title {
  411. margin: 0;
  412. font-size: 22px;
  413. font-weight: bold;
  414. }
  415. .header-room {
  416. font-size: 28px;
  417. font-weight: bold;
  418. text-align: center;
  419. flex: 1;
  420. }
  421. .header-logo {
  422. height: 60px;
  423. text-align: right;
  424. object-fit: contain;
  425. }
  426. .card-main {
  427. display: flex;
  428. flex: 1;
  429. min-height: 0;
  430. overflow: hidden;
  431. }
  432. .left-section {
  433. width: 150px;
  434. padding: 20px 10px;
  435. text-align: center;
  436. border-right: 1px solid #e0e0e0;
  437. display: flex;
  438. flex-direction: column;
  439. align-items: center;
  440. justify-content: space-between;
  441. flex-shrink: 0;
  442. }
  443. .room-number {
  444. font-size: 60px;
  445. font-weight: bold;
  446. color: #ff6b00;
  447. margin-bottom: 10px;
  448. }
  449. .bed-info {
  450. font-size: 32px;
  451. font-weight: bold;
  452. color: #ff6b00;
  453. margin-bottom: 20px;
  454. }
  455. .bed-letter {
  456. padding-top: 10px;
  457. }
  458. .qrcode-container {
  459. margin-top: 10px;
  460. }
  461. .qrcode-text {
  462. margin-top: 8px;
  463. font-size: 14px;
  464. color: #666;
  465. }
  466. .right-section {
  467. flex: 1;
  468. min-width: 0;
  469. padding: 0;
  470. height: 100%;
  471. }
  472. .info-table {
  473. width: 100%;
  474. height: 100%;
  475. border-collapse: collapse;
  476. font-size: 18px;
  477. table-layout: fixed;
  478. }
  479. .info-table tr {
  480. height: 86px;
  481. }
  482. .info-table .row-basic {
  483. height: 50px;
  484. }
  485. .info-table .row-care {
  486. height: 50px;
  487. }
  488. .info-table .row-medical {
  489. height: 70px;
  490. }
  491. .info-table .row-diet {
  492. height: 50px;
  493. }
  494. .info-table .row-prevention {
  495. height: 200px;
  496. }
  497. .info-table .row-staff {
  498. height: 60px;
  499. }
  500. .info-table td {
  501. border: 1px solid #999;
  502. padding: 10px;
  503. text-align: center;
  504. vertical-align: middle;
  505. overflow: hidden;
  506. }
  507. .table-label {
  508. font-weight: bold;
  509. background-color: #f5f5f5;
  510. width: 90px;
  511. white-space: nowrap;
  512. }
  513. .table-data {
  514. text-align: left;
  515. color: #333;
  516. overflow: hidden;
  517. white-space: wrap;
  518. }
  519. .diet-cell {
  520. padding: 0 !important;
  521. }
  522. .diet-list {
  523. display: flex;
  524. height: 100%;
  525. }
  526. .diet-item {
  527. flex: 1;
  528. display: flex;
  529. align-items: center;
  530. justify-content: center;
  531. padding: 10px;
  532. border-right: 1px solid #999;
  533. position: relative;
  534. min-height: 44px;
  535. }
  536. .diet-item:last-child {
  537. border-right: none;
  538. }
  539. .diet-item.active {
  540. background-color: #ffffff;
  541. color: #ff6b00;
  542. }
  543. .diet-name {
  544. font-size: 14px;
  545. }
  546. .diet-check {
  547. margin-right: 6px;
  548. color: #e78d35;
  549. font-weight: bold;
  550. font-size: 16px;
  551. }
  552. .diet-empty {
  553. padding: 10px;
  554. color: #999;
  555. text-align: left;
  556. min-height: 44px;
  557. display: flex;
  558. align-items: center;
  559. }
  560. .prevention-title {
  561. width: 40px;
  562. padding: 0 !important;
  563. }
  564. .vertical-text {
  565. writing-mode: vertical-rl;
  566. text-orientation: upright;
  567. letter-spacing: 4px;
  568. font-size: 18px;
  569. font-weight: bold;
  570. margin: 0 auto;
  571. }
  572. .prevention-cell {
  573. padding: 0 !important;
  574. }
  575. .prevention-grid {
  576. display: grid;
  577. grid-template-columns: repeat(5, 1fr);
  578. grid-template-rows: repeat(2, 1fr);
  579. height: 100%;
  580. }
  581. .prevention-item {
  582. border-right: 1px solid #999;
  583. border-bottom: 1px solid #999;
  584. display: flex;
  585. flex-direction: column;
  586. align-items: center;
  587. justify-content: center;
  588. padding: 6px;
  589. position: relative;
  590. }
  591. .prevention-item:nth-child(9) {
  592. border-right: none;
  593. border-bottom: none;
  594. }
  595. .prevention-item:nth-child(n + 6):nth-child(-n + 8) {
  596. border-bottom: none;
  597. }
  598. .prevention-pos-1 {
  599. grid-column: 1;
  600. grid-row: 1;
  601. }
  602. .prevention-pos-2 {
  603. grid-column: 2;
  604. grid-row: 1;
  605. }
  606. .prevention-pos-3 {
  607. grid-column: 3;
  608. grid-row: 1;
  609. }
  610. .prevention-pos-4 {
  611. grid-column: 4;
  612. grid-row: 1;
  613. }
  614. .prevention-pos-5 {
  615. grid-column: 1;
  616. grid-row: 2;
  617. }
  618. .prevention-pos-6 {
  619. grid-column: 2;
  620. grid-row: 2;
  621. }
  622. .prevention-pos-7 {
  623. grid-column: 3;
  624. grid-row: 2;
  625. }
  626. .prevention-pos-8 {
  627. grid-column: 4;
  628. grid-row: 2;
  629. }
  630. .prevention-pos-9 {
  631. grid-column: 5;
  632. grid-row: 1 / 3;
  633. background-color: #fff;
  634. }
  635. .risk-badge {
  636. position: absolute;
  637. top: 4px;
  638. left: 4px;
  639. display: flex;
  640. align-items: center;
  641. font-size: 11px;
  642. color: #e78d35;
  643. font-weight: bold;
  644. }
  645. .risk-star {
  646. margin-right: 2px;
  647. }
  648. .placeholder-image {
  649. width: 90px;
  650. object-fit: contain;
  651. display: flex;
  652. align-items: center;
  653. justify-content: center;
  654. }
  655. .placeholder-image span {
  656. font-size: 11px;
  657. color: #999;
  658. text-align: center;
  659. }
  660. .prevention-name {
  661. font-size: 12px;
  662. color: #333;
  663. text-align: center;
  664. line-height: 1.2;
  665. }
  666. .card-footer {
  667. height: 8px;
  668. flex-shrink: 0;
  669. background: linear-gradient(90deg, #e78d35 0%, #4caf50 100%);
  670. clear: both;
  671. }
  672. .export-btn {
  673. margin-top: 20px;
  674. padding: 10px 30px;
  675. background-color: #4caf50;
  676. color: white;
  677. border: none;
  678. border-radius: 4px;
  679. cursor: pointer;
  680. font-size: 16px;
  681. transition: background-color 0.3s;
  682. }
  683. .export-btn:hover {
  684. background-color: #45a049;
  685. }
  686. </style>