index.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. <template>
  2. <CustomNav title="医生首页" leftType="scan" @scan="handleScan" :opacity="0" />
  3. <view class="page-container">
  4. <view class="content">
  5. <view class="user-info">
  6. <!-- 用户信息骨架屏 -->
  7. <view v-if="userInfoLoading" class="user-info-skeleton">
  8. <view class="avatar-section">
  9. <view class="avatar">
  10. <view class="avatar-frame">
  11. <view class="skeleton-avatar-placeholder"></view>
  12. </view>
  13. </view>
  14. <view class="user-details">
  15. <view class="skeleton-line skeleton-nickname"></view>
  16. <view class="skeleton-line skeleton-title"></view>
  17. </view>
  18. <view class="message-button" @click="onMessageClick">
  19. <image src="/static/icons/remixicon/message-3-line.svg" class="message-icon" />
  20. <view class="badge" v-if="unreadMessageCount > 0">{{ unreadMessageCount > 99 ? '99+' : unreadMessageCount }}
  21. </view>
  22. </view>
  23. <view class="qr-button" @click="onQrClick">
  24. <image src="/static/icons/remixicon/qr-code-line.svg" class="qr-icon" />
  25. </view>
  26. </view>
  27. </view>
  28. <!-- 实际用户信息 -->
  29. <view v-if="!userInfoLoading" class="avatar-section">
  30. <view class="avatar">
  31. <view class="avatar-frame">
  32. <image class="avatar-img" :src="avatarSrc" mode="aspectFill" />
  33. </view>
  34. </view>
  35. <view class="user-details">
  36. <text class="username">{{ user.nickname || '未登录' }}</text>
  37. <text class="user-title" v-if="user.title">职称: {{ user.title }}</text>
  38. </view>
  39. <view class="message-button" @click="onMessageClick">
  40. <image src="/static/icons/remixicon/message-3-line.svg" class="message-icon" />
  41. <view class="badge" v-if="unreadMessageCount > 0">{{ unreadMessageCount > 99 ? '99+' : unreadMessageCount }}
  42. </view>
  43. </view>
  44. <view class="qr-button" @click="onQrClick">
  45. <image src="/static/icons/remixicon/qr-code-line.svg" class="qr-icon" />
  46. </view>
  47. </view>
  48. </view>
  49. <view class="function-container">
  50. <view class="function-row">
  51. <view class="function-item blue" @click="onItemClick('我的病人')">
  52. <view class="item-content">
  53. <view class="title-row">
  54. <view class="item-line"></view>
  55. <text class="item-title">我的病人</text>
  56. </view>
  57. <text class="item-desc">管理慢性病患者</text>
  58. </view>
  59. </view>
  60. <view class="function-item orange" @click="onItemClick('药品管理')">
  61. <view class="item-content">
  62. <view class="title-row">
  63. <view class="item-line"></view>
  64. <text class="item-title">药品管理</text>
  65. </view>
  66. <text class="item-desc">管理药品信息</text>
  67. </view>
  68. </view>
  69. </view>
  70. <view class="function-row">
  71. <view class="function-item purple" @click="onItemClick('危急值管理')">
  72. <view class="item-content">
  73. <view class="title-row">
  74. <view class="item-line"></view>
  75. <text class="item-title">危急值管理</text>
  76. </view>
  77. <text class="item-desc">设置与管理危急值阈值</text>
  78. </view>
  79. </view>
  80. <view class="function-item green" @click="onItemClick('健康资讯管理')">
  81. <view class="item-content">
  82. <view class="title-row">
  83. <view class="item-line"></view>
  84. <text class="item-title">健康资讯管理</text>
  85. </view>
  86. <text class="item-desc">管理健康资讯与文章</text>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. <!--
  92. <view class="today-reminder-card">
  93. <view class="card-header">
  94. <text class="card-title">复诊概览</text>
  95. </view>
  96. <view class="card-content">
  97. <view class="reminder-item" @click="onItemClick('复诊管理')">
  98. <view class="reminder-icon">
  99. <image src="/static/icons/remixicon/question-line.svg" class="icon" />
  100. </view>
  101. <view class="reminder-text">
  102. <text class="reminder-number">{{ todayReminders.pendingCount }}</text>
  103. <text class="reminder-label">待确认复诊</text>
  104. </view>
  105. </view>
  106. <view class="reminder-item" @click="onItemClick('复诊管理')">
  107. <view class="reminder-icon">
  108. <image src="/static/icons/remixicon/time-line.svg" class="icon" />
  109. </view>
  110. <view class="reminder-text">
  111. <text class="reminder-number">{{ todayReminders.confirmedCount }}</text>
  112. <text class="reminder-label">待完成复诊</text>
  113. </view>
  114. </view>
  115. </view>
  116. </view>
  117. -->
  118. <view class="patient-activity-card">
  119. <view class="card-header">
  120. <text class="card-title">患者动态</text>
  121. </view>
  122. <view class="activity-card-content">
  123. <!-- 骨架屏 -->
  124. <view v-if="patientActivitiesLoading" class="skeleton-container">
  125. <view class="skeleton-item" v-for="i in 3" :key="i">
  126. <view class="skeleton-avatar"></view>
  127. <view class="skeleton-text">
  128. <view class="skeleton-line skeleton-desc"></view>
  129. <view class="skeleton-line skeleton-time"></view>
  130. </view>
  131. </view>
  132. </view>
  133. <!-- 实际内容 -->
  134. <view v-if="!patientActivitiesLoading">
  135. <view class="activity-item" v-for="(activity, index) in patientActivities" :key="index">
  136. <view class="activity-avatar">
  137. <image :src="activity.patientAvatar" class="avatar-img" mode="aspectFill" />
  138. </view>
  139. <view class="activity-text">
  140. <text class="activity-desc">{{ activity.desc }}</text>
  141. <text class="activity-time">{{ activity.time }}</text>
  142. </view>
  143. </view>
  144. <view v-if="patientActivities.length === 0 && !patientActivitiesError" class="no-activity">
  145. <text>暂无患者动态</text>
  146. </view>
  147. <view v-if="patientActivitiesError" class="error-activity">
  148. <text class="error-text">加载失败,请重试</text>
  149. <view class="retry-button" @click="fetchPatientActivities">
  150. <text class="retry-text">重试</text>
  151. </view>
  152. </view>
  153. </view>
  154. </view>
  155. </view>
  156. </view>
  157. </view>
  158. <TabBar />
  159. </template>
  160. <script setup lang="ts">
  161. import { ref, computed } from 'vue'
  162. import { onShow, onHide } from '@dcloudio/uni-app'
  163. import CustomNav from '@/components/custom-nav.vue'
  164. import TabBar from '@/components/tab-bar.vue'
  165. import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
  166. import request from '@/api/request'
  167. import { handleQrScanResult } from '@/utils/qr'
  168. import { queryBoundPatientsActivities } from '@/api/userActivity'
  169. import { avatarCache } from '@/utils/avatarCache'
  170. import { getUnreadMessageCount, getMessageList, markMessageAsRead, type MessageQueryParams } from '@/api/message'
  171. const user = ref<{ avatar?: string; nickname?: string; title?: string }>({})
  172. // 用户信息加载状态
  173. const userInfoLoading = ref(true)
  174. // 未读消息数量
  175. const unreadMessageCount = ref(0)
  176. // 页面活跃状态标志
  177. const isPageActive = ref(false)
  178. const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
  179. const avatarSrc = computed(() => {
  180. const a = user.value?.avatar
  181. if (!a) return defaultAvatarUrl
  182. try {
  183. const s = String(a)
  184. if (/^(https?:\/\/|data:|wxfile:\/\/|file:\/\/|\/static\/)/i.test(s)) {
  185. return s
  186. }
  187. if (/^(\.|\/|temp)/i.test(s)) return s
  188. } catch (e) {
  189. // fallback
  190. }
  191. return defaultAvatarUrl
  192. })
  193. /*
  194. const todayReminders = ref({
  195. pendingCount: 0,
  196. confirmedCount: 0
  197. })
  198. */
  199. const patientActivities = ref<Array<{
  200. desc: string
  201. time: string
  202. patientAvatar: string
  203. }>>([])
  204. // 患者动态加载状态
  205. const patientActivitiesLoading = ref(true)
  206. // 患者动态获取失败状态
  207. const patientActivitiesError = ref(false)
  208. const loadUser = () => {
  209. try {
  210. const u = uni.getStorageSync('user_info')
  211. if (u) {
  212. user.value = u
  213. }
  214. } catch (e) {
  215. // ignore
  216. }
  217. }
  218. const fetchUserInfo = async () => {
  219. try {
  220. userInfoLoading.value = true
  221. const token = uni.getStorageSync('token')
  222. if (!token) {
  223. userInfoLoading.value = false
  224. return
  225. }
  226. const response = await fetchUserInfoApi()
  227. uni.hideLoading()
  228. console.log('User info response:', response)
  229. const resp = response.data as any
  230. if (response.statusCode === 401) {
  231. // Token 无效,清除并跳转登录
  232. uni.removeStorageSync('token')
  233. uni.removeStorageSync('role')
  234. user.value = {}
  235. uni.reLaunch({ url: '/pages/public/login/index' })
  236. userInfoLoading.value = false
  237. return
  238. }
  239. if (resp && resp.code === 200 && resp.data) {
  240. // 如果头像无效(不是有效的 http URL),则下载头像
  241. if (!resp.data.avatar || !resp.data.avatar.startsWith('http')) {
  242. const userIdRaw = resp.data.id || resp.data.userId
  243. const userId = userIdRaw ? String(userIdRaw) : ''
  244. if (userId) {
  245. try {
  246. // 使用 avatarCache.getOrFetch 做并发去重,loader 只在真正需要下载时被调用一次
  247. const path = await avatarCache.getOrFetch(userId, async (id) => {
  248. try {
  249. const downloadRes = await uni.downloadFile({
  250. url: `https://wx.baiyun.work/user/avatar/${id}`,
  251. header: {
  252. Authorization: `Bearer ${token}`
  253. }
  254. })
  255. if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
  256. return downloadRes.tempFilePath
  257. }
  258. } catch (e) {
  259. console.error('Download avatar error:', e)
  260. }
  261. return undefined
  262. })
  263. if (path) resp.data.avatar = path
  264. } catch (e) {
  265. console.error('avatar fetch error:', e)
  266. }
  267. }
  268. }
  269. user.value = resp.data
  270. uni.setStorageSync('user_info', resp.data)
  271. if (!resp.data.nickname || !resp.data.avatar) {
  272. uni.navigateTo({ url: '/pages/doctor/profile/index' })
  273. }
  274. }
  275. } catch (err) {
  276. uni.hideLoading()
  277. console.error('Fetch user info error:', err)
  278. } finally {
  279. userInfoLoading.value = false
  280. }
  281. }
  282. /*
  283. const fetchTodayReminders = async () => {
  284. try {
  285. const token = uni.getStorageSync('token')
  286. if (!token) return
  287. // 暂时移除复诊数据获取逻辑
  288. todayReminders.value = {
  289. pendingCount: 0,
  290. confirmedCount: 0
  291. }
  292. } catch (err) {
  293. console.error('Fetch today reminders error:', err)
  294. }
  295. }
  296. */
  297. const fetchPatientActivities = async () => {
  298. try {
  299. patientActivitiesLoading.value = true
  300. patientActivitiesError.value = false
  301. const token = uni.getStorageSync('token')
  302. if (!token) {
  303. console.log('No token found, skipping fetchPatientActivities')
  304. patientActivitiesLoading.value = false
  305. return
  306. }
  307. console.log('Fetching patient activities...')
  308. // 调用真实接口获取患者动态
  309. const response = await queryBoundPatientsActivities({
  310. pageNum: 1,
  311. pageSize: 10
  312. })
  313. console.log('Patient activities response:', response)
  314. const resp = response.data as any
  315. if (resp && resp.code === 200 && resp.data) {
  316. console.log('Patient activities data:', resp.data)
  317. // 转换数据格式,异步获取头像
  318. const activitiesPromises = resp.data.records.map(async (activity: any) => ({
  319. desc: formatActivityDescription(activity),
  320. time: formatTime(activity.createTime),
  321. // 强制把 userId 转为字符串,避免数值精度或类型不匹配引起的问题
  322. patientAvatar: await getPatientAvatar(String(activity.userId))
  323. }))
  324. // 等待所有头像获取完成
  325. const activities = await Promise.all(activitiesPromises)
  326. console.log('Converted activities:', activities)
  327. patientActivities.value = activities
  328. } else {
  329. console.log('No patient activities data or invalid response')
  330. patientActivities.value = []
  331. }
  332. } catch (err) {
  333. console.error('Fetch patient activities error:', err)
  334. // 如果接口调用失败,显示错误状态
  335. patientActivitiesError.value = true
  336. patientActivities.value = []
  337. } finally {
  338. patientActivitiesLoading.value = false
  339. }
  340. }
  341. // 获取未读消息数量
  342. const fetchUnreadMessageCount = async () => {
  343. try {
  344. const response = await getUnreadMessageCount()
  345. const resp = response.data as any
  346. if (resp && resp.code === 200) {
  347. unreadMessageCount.value = resp.data || 0
  348. }
  349. } catch (error) {
  350. console.error('获取未读消息数量失败:', error)
  351. }
  352. }
  353. // 检查并显示需要弹窗的消息
  354. const checkAndShowPopupMessages = async () => {
  355. try {
  356. // 获取最近的消息,检查是否有需要弹窗的未读消息
  357. const params: MessageQueryParams = {
  358. page: 1,
  359. size: 10,
  360. status: 0 // 未读消息
  361. }
  362. const response = await getMessageList(params)
  363. const resp = response.data as any
  364. if (resp && resp.code === 200 && resp.data) {
  365. const messages = resp.data.records || []
  366. // 查找需要弹窗的消息(notifyPopup为true且未读)
  367. const popupMessages = messages.filter((msg: any) => msg.notifyPopup && msg.status === 0)
  368. if (popupMessages.length > 0) {
  369. // 显示最新的弹窗消息
  370. const latestMessage = popupMessages[0]
  371. // 延迟一点时间显示弹窗,确保页面加载完成
  372. setTimeout(() => {
  373. // 检查页面是否仍然活跃
  374. if (!isPageActive.value) {
  375. return
  376. }
  377. uni.showModal({
  378. title: getMessageTitle(latestMessage.type),
  379. content: latestMessage.content,
  380. showCancel: false,
  381. confirmText: '我知道了',
  382. success: () => {
  383. // 用户确认后,自动标记为已读
  384. markMessageAsReadLocal(latestMessage.id)
  385. }
  386. })
  387. }, 1000)
  388. }
  389. }
  390. } catch (error) {
  391. console.error('检查弹窗消息失败:', error)
  392. }
  393. }
  394. // 标记消息已读
  395. const markMessageAsReadLocal = async (messageId: string) => {
  396. try {
  397. await markMessageAsRead(messageId)
  398. // 更新未读消息数量
  399. if (unreadMessageCount.value > 0) {
  400. unreadMessageCount.value--
  401. }
  402. } catch (error) {
  403. console.error('标记消息已读失败:', error)
  404. }
  405. }
  406. // 获取消息标题
  407. const getMessageTitle = (type: string) => {
  408. switch (type) {
  409. case 'SYSTEM_DAILY':
  410. return '系统通知'
  411. case 'DOCTOR':
  412. return '医生消息'
  413. case 'SYSTEM_ANOMALY':
  414. return '异常通知'
  415. default:
  416. return '消息提醒'
  417. }
  418. }
  419. // 格式化时间显示
  420. const formatTime = (createTime: string) => {
  421. try {
  422. const now = new Date()
  423. const create = new Date(createTime)
  424. const diff = now.getTime() - create.getTime()
  425. const minutes = Math.floor(diff / (1000 * 60))
  426. const hours = Math.floor(diff / (1000 * 60 * 60))
  427. const days = Math.floor(diff / (1000 * 60 * 60 * 24))
  428. if (minutes < 1) return '刚刚'
  429. if (minutes < 60) return `${minutes}分钟前`
  430. if (hours < 24) return `${hours}小时前`
  431. if (days < 7) return `${days}天前`
  432. return create.toLocaleDateString('zh-CN')
  433. } catch (e) {
  434. return createTime
  435. }
  436. }
  437. function onMessageClick() {
  438. uni.navigateTo({
  439. url: '/pages/public/message-detail',
  440. events: {
  441. // 监听消息详情页的消息已读事件
  442. messageRead: () => {
  443. fetchUnreadMessageCount()
  444. }
  445. }
  446. })
  447. }
  448. // 获取患者头像
  449. const getPatientAvatar = async (userId: string | number): Promise<string> => {
  450. try {
  451. const token = uni.getStorageSync('token')
  452. if (!token) return defaultAvatarUrl
  453. // 使用 getOrFetch 去重并缓存下载结果
  454. const idStr = String(userId)
  455. try {
  456. const path = await avatarCache.getOrFetch(idStr, async (id) => {
  457. try {
  458. const downloadRes = await uni.downloadFile({
  459. url: `https://wx.baiyun.work/user/avatar/${id}`,
  460. header: {
  461. Authorization: `Bearer ${token}`
  462. }
  463. })
  464. if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
  465. return downloadRes.tempFilePath
  466. }
  467. } catch (e) {
  468. console.error('Download patient avatar error:', e)
  469. }
  470. return undefined
  471. })
  472. if (path) return path
  473. } catch (e) {
  474. console.error('getPatientAvatar error:', e)
  475. }
  476. } catch (e) {
  477. console.error('Download patient avatar error:', e)
  478. }
  479. // 如果获取失败,使用默认头像
  480. return defaultAvatarUrl
  481. }
  482. // 如果在微信小程序端且未登录,自动跳转到登录页
  483. onShow(() => {
  484. isPageActive.value = true
  485. const token = uni.getStorageSync('token')
  486. if (!token) {
  487. // 使用 uni.reLaunch 替代 navigateTo,确保页面栈被清空
  488. uni.reLaunch({ url: '/pages/public/login/index' })
  489. } else {
  490. fetchUserInfo()
  491. // fetchTodayReminders()
  492. fetchPatientActivities()
  493. fetchUnreadMessageCount()
  494. checkAndShowPopupMessages()
  495. }
  496. })
  497. // 页面隐藏时设置不活跃
  498. onHide(() => {
  499. isPageActive.value = false
  500. })
  501. function handleScan(res: any) {
  502. return handleQrScanResult(res)
  503. }
  504. function onItemClick(type: string) {
  505. switch (type) {
  506. case '我的病人':
  507. uni.navigateTo({ url: '/pages/doctor/index/my-patients' })
  508. break
  509. case '药品管理':
  510. uni.navigateTo({ url: '/pages/doctor/manage/medicine' })
  511. break
  512. case '健康资讯管理':
  513. uni.navigateTo({ url: '/pages/doctor/manage/news' })
  514. break
  515. case '危急值管理':
  516. uni.navigateTo({ url: '/pages/doctor/manage/critical-values' })
  517. break
  518. default:
  519. uni.showToast({ title: '功能正在开发中', icon: 'none' })
  520. }
  521. }
  522. function onQrClick() {
  523. uni.navigateTo({ url: '/pages/public/profile/qr/index' })
  524. }
  525. // 格式化活动描述
  526. const formatActivityDescription = (activity: any) => {
  527. // 如果已经有友好的描述,直接返回
  528. if (activity.friendlyDescription) {
  529. return activity.friendlyDescription
  530. }
  531. // 根据 activityDescription 内容进一步细化描述
  532. const description = activity.activityDescription || ''
  533. let baseDescription = ''
  534. // 根据活动类型生成基础描述
  535. switch (activity.activityType) {
  536. case 'BLOOD_GLUCOSE_UPLOAD':
  537. baseDescription = '上传了血糖数据'
  538. break
  539. case 'BLOOD_GLUCOSE_UPDATE':
  540. baseDescription = '更新了血糖数据'
  541. break
  542. case 'BLOOD_PRESSURE_UPLOAD':
  543. baseDescription = '上传了血压数据'
  544. break
  545. case 'HEART_RATE_UPLOAD':
  546. baseDescription = '上传了心率数据'
  547. break
  548. case 'PHYSICAL_DATA_UPLOAD':
  549. baseDescription = '上传了体格数据'
  550. break
  551. case 'HEALTH_RECORD_CREATE':
  552. // 根据描述内容判断是创建还是更新
  553. if (description.includes('save') || description.includes('update')) {
  554. baseDescription = '更新了健康档案'
  555. } else {
  556. baseDescription = '创建了健康档案'
  557. }
  558. break
  559. case 'HEALTH_RECORD_UPDATE':
  560. baseDescription = '更新了健康档案'
  561. break
  562. case 'MEDICATION_CREATE':
  563. baseDescription = '添加了用药记录'
  564. break
  565. case 'MEDICATION_UPDATE':
  566. baseDescription = '更新了用药记录'
  567. break
  568. case 'USER_BINDING_CREATE':
  569. baseDescription = '绑定了新患者'
  570. break
  571. case 'USER_BINDING_DELETE':
  572. baseDescription = '解除了患者绑定'
  573. break
  574. default:
  575. // 如果没有匹配的类型,尝试使用 activityDescription 或返回默认值
  576. baseDescription = description && !description.includes('Controller')
  577. ? description
  578. : '执行了操作'
  579. }
  580. return baseDescription
  581. }
  582. </script>
  583. <style>
  584. .page-container {
  585. min-height: 100vh;
  586. padding-top: calc(var(--status-bar-height) + 44px);
  587. padding-bottom: 100rpx;
  588. box-sizing: border-box;
  589. justify-content: center;
  590. align-items: center;
  591. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  592. }
  593. .content {
  594. width: 100%;
  595. }
  596. .user-info {
  597. /* background-color: #fff; */
  598. padding: 40rpx;
  599. margin-top: 0rpx;
  600. }
  601. .avatar-section {
  602. display: flex;
  603. align-items: center;
  604. }
  605. .avatar {
  606. width: 120rpx;
  607. height: 120rpx;
  608. border-radius: 50%;
  609. border: 1px solid rgba(128, 128, 128, 0.5);
  610. display: flex;
  611. align-items: center;
  612. justify-content: center;
  613. margin-right: 30rpx;
  614. }
  615. .avatar-frame {
  616. width: 100%;
  617. height: 100%;
  618. border-radius: 50%;
  619. overflow: hidden;
  620. display: flex;
  621. align-items: center;
  622. justify-content: center;
  623. }
  624. .avatar-img {
  625. width: 100%;
  626. height: 100%;
  627. object-fit: cover;
  628. }
  629. .user-details {
  630. flex: 1;
  631. }
  632. .username {
  633. font-size: 36rpx;
  634. font-weight: bold;
  635. color: #333;
  636. display: block;
  637. margin-bottom: 10rpx;
  638. }
  639. .user-title {
  640. font-size: 28rpx;
  641. color: #666;
  642. display: block;
  643. margin-bottom: 10rpx;
  644. }
  645. .qr-button {
  646. width: 100rpx;
  647. height: 100rpx;
  648. border-radius: 50%;
  649. background-color: rgba(255, 255, 255, 0.5);
  650. display: flex;
  651. align-items: center;
  652. justify-content: center;
  653. margin-left: 20rpx;
  654. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  655. }
  656. .qr-icon {
  657. width: 60rpx;
  658. height: 60rpx;
  659. }
  660. .message-button {
  661. width: 100rpx;
  662. height: 100rpx;
  663. border-radius: 50%;
  664. background-color: rgba(255, 255, 255, 0.5);
  665. display: flex;
  666. align-items: center;
  667. justify-content: center;
  668. margin-left: 20rpx;
  669. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
  670. position: relative;
  671. }
  672. .message-icon {
  673. width: 60rpx;
  674. height: 60rpx;
  675. }
  676. .badge {
  677. position: absolute;
  678. top: -10rpx;
  679. right: -10rpx;
  680. background-color: red;
  681. color: white;
  682. border-radius: 50%;
  683. width: 40rpx;
  684. height: 40rpx;
  685. display: flex;
  686. align-items: center;
  687. justify-content: center;
  688. font-size: 24rpx;
  689. font-weight: bold;
  690. }
  691. .function-container {
  692. padding-inline: 20rpx;
  693. }
  694. .function-row {
  695. display: flex;
  696. justify-content: space-between;
  697. margin-bottom: 20rpx;
  698. }
  699. .function-row:last-child {
  700. margin-bottom: 0;
  701. }
  702. .function-item {
  703. flex: 1;
  704. height: 160rpx;
  705. background-color: #fff;
  706. border-radius: 20rpx;
  707. margin: 0 10rpx;
  708. position: relative;
  709. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
  710. overflow: hidden;
  711. }
  712. .item-content {
  713. position: absolute;
  714. top: 20rpx;
  715. left: 20rpx;
  716. display: flex;
  717. flex-direction: column;
  718. }
  719. .title-row {
  720. display: flex;
  721. align-items: center;
  722. margin-bottom: 10rpx;
  723. }
  724. .item-line {
  725. width: 8rpx;
  726. height: 48rpx;
  727. margin-right: 15rpx;
  728. border-radius: 10rpx;
  729. }
  730. .blue .item-line {
  731. background-color: #3742fa;
  732. }
  733. .orange .item-line {
  734. background-color: #ffa502;
  735. }
  736. .green .item-line {
  737. background-color: #2ecc71;
  738. }
  739. .purple .item-line {
  740. background-color: #9b59b6;
  741. }
  742. .item-title {
  743. font-size: 36rpx;
  744. font-weight: bold;
  745. color: #333;
  746. }
  747. .item-desc {
  748. font-size: 28rpx;
  749. color: #666;
  750. }
  751. /*
  752. 以下为复诊概览卡片样式,已注释以便临时隐藏该功能
  753. .today-reminder-card { ... }
  754. .card-header { ... }
  755. .card-title { ... }
  756. .card-content { ... }
  757. .reminder-item { ... }
  758. .reminder-icon { ... }
  759. .icon { ... }
  760. .reminder-text { ... }
  761. .reminder-number { ... }
  762. .reminder-label { ... }
  763. */
  764. .activity-card-content {
  765. padding: 20rpx;
  766. display: block;
  767. }
  768. .patient-activity-card {
  769. background-color: #fff;
  770. border-radius: 20rpx;
  771. margin: 20rpx;
  772. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
  773. overflow: hidden;
  774. }
  775. /* 卡片头样式(用于患者动态等卡片) */
  776. .card-header {
  777. padding: 24rpx 28rpx;
  778. display: flex;
  779. align-items: center;
  780. background: linear-gradient(90deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.75));
  781. border-bottom: 1rpx solid #f3f4f6;
  782. }
  783. .card-title {
  784. font-size: 32rpx;
  785. font-weight: 700;
  786. color: #222;
  787. }
  788. .activity-item {
  789. display: flex;
  790. align-items: center;
  791. margin-bottom: 20rpx;
  792. padding: 20rpx;
  793. border-bottom: 1rpx solid #eee;
  794. }
  795. .activity-item:last-child {
  796. margin-bottom: 0;
  797. border-bottom: none;
  798. }
  799. .activity-avatar {
  800. width: 60rpx;
  801. height: 60rpx;
  802. border-radius: 50%;
  803. border: 1px solid rgba(128, 128, 128, 0.5);
  804. margin-right: 20rpx;
  805. overflow: hidden;
  806. display: flex;
  807. align-items: center;
  808. justify-content: center;
  809. }
  810. .activity-text {
  811. flex: 1;
  812. }
  813. .activity-desc {
  814. font-size: 32rpx;
  815. color: #333;
  816. display: block;
  817. margin-bottom: 10rpx;
  818. }
  819. .activity-time {
  820. font-size: 28rpx;
  821. color: #666;
  822. }
  823. .no-activity {
  824. padding: 40rpx;
  825. text-align: center;
  826. color: #999;
  827. }
  828. .error-activity {
  829. padding: 40rpx;
  830. text-align: center;
  831. display: flex;
  832. flex-direction: column;
  833. align-items: center;
  834. }
  835. .error-text {
  836. color: #ff6b6b;
  837. font-size: 28rpx;
  838. margin-bottom: 20rpx;
  839. }
  840. .retry-button {
  841. background-color: #007aff;
  842. color: white;
  843. padding: 16rpx 32rpx;
  844. border-radius: 8rpx;
  845. display: inline-block;
  846. }
  847. .retry-text {
  848. font-size: 28rpx;
  849. color: white;
  850. }
  851. /* 骨架屏样式 */
  852. .skeleton-container {
  853. padding: 20rpx;
  854. }
  855. .skeleton-item {
  856. display: flex;
  857. align-items: center;
  858. margin-bottom: 20rpx;
  859. padding: 20rpx;
  860. border-bottom: 1rpx solid #eee;
  861. }
  862. .skeleton-item:last-child {
  863. margin-bottom: 0;
  864. border-bottom: none;
  865. }
  866. .skeleton-avatar {
  867. width: 60rpx;
  868. height: 60rpx;
  869. border-radius: 50%;
  870. background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  871. background-size: 200% 100%;
  872. animation: skeleton-loading 1.5s infinite;
  873. margin-right: 20rpx;
  874. }
  875. .skeleton-text {
  876. flex: 1;
  877. }
  878. .skeleton-line {
  879. background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  880. background-size: 200% 100%;
  881. animation: skeleton-loading 1.5s infinite;
  882. border-radius: 6rpx;
  883. }
  884. .skeleton-desc {
  885. width: 70%;
  886. height: 32rpx;
  887. margin-bottom: 10rpx;
  888. }
  889. .skeleton-time {
  890. width: 40%;
  891. height: 28rpx;
  892. }
  893. /* 用户信息骨架屏样式 */
  894. .user-info-skeleton .avatar-section {
  895. display: flex;
  896. align-items: center;
  897. }
  898. .user-info-skeleton .avatar {
  899. width: 120rpx;
  900. height: 120rpx;
  901. border-radius: 50%;
  902. border: 1px solid rgba(128, 128, 128, 0.5);
  903. display: flex;
  904. align-items: center;
  905. justify-content: center;
  906. margin-right: 30rpx;
  907. }
  908. .skeleton-avatar-placeholder {
  909. width: 100%;
  910. height: 100%;
  911. border-radius: 50%;
  912. background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  913. background-size: 200% 100%;
  914. animation: skeleton-loading 1.5s infinite;
  915. }
  916. .user-info-skeleton .user-details {
  917. flex: 1;
  918. }
  919. .skeleton-nickname {
  920. width: 60%;
  921. height: 36rpx;
  922. background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  923. background-size: 200% 100%;
  924. animation: skeleton-loading 1.5s infinite;
  925. border-radius: 6rpx;
  926. margin-bottom: 10rpx;
  927. }
  928. .skeleton-title {
  929. width: 40%;
  930. height: 28rpx;
  931. background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  932. background-size: 200% 100%;
  933. animation: skeleton-loading 1.5s infinite;
  934. border-radius: 6rpx;
  935. }
  936. @keyframes skeleton-loading {
  937. 0% {
  938. background-position: 200% 0;
  939. }
  940. 100% {
  941. background-position: -200% 0;
  942. }
  943. }
  944. </style>