index.vue 18 KB

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