ElderlyCard.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <template>
  2. <div
  3. :class="['elderly-card-large', { active: isActive, flashing: elderly._flashEffect }]"
  4. @click="selectElderly"
  5. >
  6. <div v-if="hasWarning" class="warning-action-group">
  7. <Icon icon="mdi:alert" class="warning-flag" color="red" :size="50" />
  8. <el-button type="warning" size="small" class="handle-warning-btn" @click.stop="handleWarning">
  9. 去处理
  10. </el-button>
  11. </div>
  12. <div class="elderly-avatar-large" :class="getGenderClass(elderly.gender)">
  13. <span class="avatar-initial">{{ getNameInitial(elderly.name) }}</span>
  14. </div>
  15. <div class="elderly-info-large">
  16. <h3>{{ elderly.name }}</h3>
  17. <p>{{ elderly.age || 0 }}岁 • {{ genderMap[elderly.gender] || '未知' }}</p>
  18. <div class="health-status-large">
  19. <div class="status-dot" :class="getHealthStatusClass(elderly.healthText)"></div>
  20. <span>{{ elderly.healthText || '未知' }}</span>
  21. </div>
  22. <div v-if="elderly.address" class="address-row" :title="elderly.address">
  23. <Icon icon="mdi:map-marker" :size="16" />
  24. <span class="address-text">{{ elderly.address }}</span>
  25. </div>
  26. </div>
  27. <div class="elderly-actions">
  28. <div class="device-count">
  29. <span>{{ elderly.deviceNumber || 0 }} 个设备</span>
  30. </div>
  31. <el-button
  32. type="primary"
  33. size="small"
  34. class="add-device-btn"
  35. style="padding: 10px !important"
  36. @click.stop="addDevice"
  37. >
  38. 添加设备
  39. </el-button>
  40. </div>
  41. </div>
  42. </template>
  43. <script lang="ts" setup>
  44. interface Elderly {
  45. id: number
  46. avatar: string
  47. name: string
  48. age: number
  49. gender: string
  50. healthStatus: string
  51. healthText: string
  52. address?: string
  53. deviceNumber: number
  54. _flashEffect?: boolean
  55. }
  56. interface Props {
  57. elderly: Elderly
  58. isActive: boolean
  59. hasWarning: boolean
  60. }
  61. const props = defineProps<Props>()
  62. const emit = defineEmits<{
  63. select: [elderly: Elderly]
  64. addDevice: [elderly: Elderly]
  65. handleWarning: [elderly: Elderly]
  66. }>()
  67. const genderMap = {
  68. 0: '女',
  69. 1: '男'
  70. }
  71. const getGenderClass = (gender: string | number) => {
  72. const maleVals = [1, '1', '男', 'male', 'M', 'm']
  73. const femaleVals = [0, '0', '女', 'female', 'F', 'f']
  74. if (maleVals.includes(gender as any)) return 'male'
  75. if (femaleVals.includes(gender as any)) return 'female'
  76. return 'unknown'
  77. }
  78. const getNameInitial = (name?: string) => {
  79. if (!name) return '?'
  80. const s = String(name).trim()
  81. return s ? s[0] : '?'
  82. }
  83. const getHealthStatusClass = (healthText: string) => {
  84. if (!healthText) return ''
  85. if (healthText.includes('良好') || healthText.includes('稳定')) return 'good'
  86. if (healthText.includes('偏高') || healthText.includes('关注') || healthText.includes('严重'))
  87. return 'warning'
  88. if (healthText.includes('危险')) return 'error'
  89. return 'normal'
  90. }
  91. const selectElderly = () => {
  92. emit('select', props.elderly)
  93. }
  94. const addDevice = () => {
  95. emit('addDevice', props.elderly)
  96. }
  97. const handleWarning = () => {
  98. emit('handleWarning', props.elderly)
  99. }
  100. </script>
  101. <style lang="scss" scoped>
  102. $primary-color: #1a73e8;
  103. $text-light: #fff;
  104. $text-gray: #8a8f98;
  105. $success-color: #26de81;
  106. $warning-color: #fd9644;
  107. $danger-color: #ff6b6b;
  108. @keyframes borderFlash {
  109. 0%,
  110. 100% {
  111. border-color: rgb(255 255 255 / 8%);
  112. box-shadow: 0 0 0 0 rgb(255 107 107 / 0%);
  113. }
  114. 50% {
  115. border-color: $danger-color;
  116. box-shadow: 0 0 20px 5px rgb(255 107 107 / 50%);
  117. }
  118. }
  119. .elderly-card-large {
  120. position: relative;
  121. display: flex;
  122. padding: 20px;
  123. cursor: pointer;
  124. background: rgb(255 255 255 / 5%);
  125. border: 1px solid rgb(255 255 255 / 8%);
  126. border-radius: 12px;
  127. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  128. align-items: center;
  129. gap: 15px;
  130. &:hover,
  131. &.active {
  132. background: rgb(26 115 232 / 20%);
  133. border-color: rgb(26 115 232 / 50%);
  134. transform: translateX(5px);
  135. }
  136. &.flashing {
  137. position: relative;
  138. z-index: 1;
  139. animation: borderFlash 1s ease-in-out infinite;
  140. }
  141. }
  142. .elderly-avatar-large {
  143. display: flex;
  144. width: 60px;
  145. height: 60px;
  146. background: rgb(255 255 255 / 10%);
  147. border-radius: 50%;
  148. align-items: center;
  149. justify-content: center;
  150. color: #fff;
  151. user-select: none;
  152. flex-shrink: 0;
  153. :deep(svg) {
  154. width: 32px !important;
  155. height: 32px !important;
  156. }
  157. &.male {
  158. background: #409eff;
  159. }
  160. &.female {
  161. background: #ff69b4;
  162. }
  163. &.unknown {
  164. background: #909399;
  165. }
  166. .avatar-initial {
  167. font-size: 22px;
  168. font-weight: 700;
  169. color: #fff;
  170. width: auto !important;
  171. height: auto !important;
  172. line-height: 1;
  173. }
  174. }
  175. .elderly-info-large {
  176. flex: 1;
  177. h3 {
  178. margin-bottom: 5px;
  179. font-size: 20px;
  180. }
  181. p {
  182. margin-bottom: 8px;
  183. color: $text-gray;
  184. }
  185. }
  186. .health-status-large {
  187. display: flex;
  188. align-items: center;
  189. gap: 8px;
  190. }
  191. .address-row {
  192. display: flex;
  193. align-items: center;
  194. gap: 6px;
  195. color: $text-gray;
  196. margin-top: 6px;
  197. }
  198. .address-text {
  199. flex: 1;
  200. min-width: 0;
  201. overflow: hidden;
  202. text-overflow: ellipsis;
  203. white-space: nowrap;
  204. }
  205. .status-dot {
  206. width: 12px;
  207. height: 12px;
  208. border-radius: 50%;
  209. box-shadow: 0 0 10px currentcolor;
  210. &.good {
  211. color: $success-color;
  212. background: $success-color;
  213. }
  214. &.warning {
  215. color: $warning-color;
  216. background: $warning-color;
  217. }
  218. &.error {
  219. color: $danger-color;
  220. background: $danger-color;
  221. }
  222. &.normal {
  223. color: $primary-color;
  224. background: $primary-color;
  225. }
  226. }
  227. .elderly-actions {
  228. display: flex;
  229. flex-direction: column;
  230. align-items: flex-end;
  231. gap: 8px;
  232. }
  233. .device-count {
  234. padding: 5px 10px;
  235. font-size: 14px;
  236. background: rgb(255 255 255 / 10%);
  237. border-radius: 20px;
  238. }
  239. .add-device-btn {
  240. white-space: nowrap;
  241. }
  242. .warning-action-group {
  243. position: absolute;
  244. top: 5px;
  245. left: 160px;
  246. z-index: 2;
  247. display: flex;
  248. align-items: center;
  249. gap: 10px;
  250. }
  251. .handle-warning-btn {
  252. white-space: nowrap;
  253. font-size: 14px;
  254. padding: 8px 16px !important;
  255. }
  256. </style>