index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <template>
  2. <CustomNav title="二维码" leftType="back" :opacity="0" />
  3. <view class="page-container">
  4. <view class="content">
  5. <view class="qr-card">
  6. <view class="user-info">
  7. <view class="title-row">
  8. <text class="card-title">{{ getPageTitle(user.role) }}</text>
  9. <view class="role-badge">{{ getRoleText(user.role) }}</view>
  10. </view>
  11. <text class="user-name">{{ user.nickname || '未知用户' }}</text>
  12. <text class="qr-desc">{{ getPageDesc(user.role) }}</text>
  13. </view>
  14. <view class="qr-container">
  15. <canvas
  16. canvas-id="qrcode"
  17. class="qr-canvas"
  18. v-if="qrData"
  19. ></canvas>
  20. </view>
  21. <view class="refresh-btn" @click="refreshAndGenerate">
  22. <text class="refresh-text">刷新二维码</text>
  23. </view>
  24. </view>
  25. </view>
  26. </view>
  27. </template>
  28. <script setup lang="ts">
  29. import { ref, onMounted } from 'vue'
  30. import { getWindowWidth, rpxToPx } from '@/utils/platform'
  31. import { onShow } from '@dcloudio/uni-app'
  32. import CustomNav from '@/components/custom-nav.vue'
  33. // @ts-ignore
  34. import drawQrcode from 'weapp-qrcode'
  35. import { fetchUserInfo as fetchUserInfoApi } from '@/api/user'
  36. const user = ref<{ nickname?: string; role?: string | number; openid?: string; wx_openid?: string }>({})
  37. const qrData = ref('')
  38. const isFetching = ref(false)
  39. const getPageTitle = (role: string | number | undefined) => {
  40. const roleMap: { [key: number]: string } = {
  41. 2: '医生身份码',
  42. 3: '个人身份码',
  43. 4: '家属身份码'
  44. }
  45. return roleMap[Number(role)] || '身份码'
  46. }
  47. const getPageDesc = (role: string | number | undefined) => {
  48. const roleMap: { [key: number]: string } = {
  49. 2: '扫码绑定医生',
  50. 3: '扫码绑定患者或查看健康档案',
  51. 4: '扫码查看相关患者健康档案'
  52. }
  53. return roleMap[Number(role)] || '扫码查看信息'
  54. }
  55. const getRoleText = (role: string | number | undefined) => {
  56. const roleMap: { [key: number]: string } = {
  57. 2: '医生码',
  58. 3: '患者码',
  59. 4: '家属码'
  60. }
  61. return roleMap[Number(role)] || '身份码'
  62. }
  63. const loadUser = () => {
  64. try {
  65. const u = uni.getStorageSync('user_info')
  66. console.log('Loaded user_info from storage:', u)
  67. if (u) {
  68. user.value = u
  69. console.log('User data set:', user.value)
  70. }
  71. } catch (e) {
  72. console.error('Error loading user info:', e)
  73. }
  74. }
  75. // 从后端刷新用户信息(仅在用户手动触发刷新时调用)
  76. const fetchUserInfo = async () => {
  77. try {
  78. const token = uni.getStorageSync('token')
  79. if (!token) return
  80. if (isFetching.value) return
  81. isFetching.value = true
  82. uni.showLoading({ title: '加载中...' })
  83. const response: any = await fetchUserInfoApi()
  84. uni.hideLoading()
  85. console.log('User info response:', response)
  86. const resp = response.data as any
  87. if (response.statusCode === 401) {
  88. // Token 无效,清除并跳转登录
  89. uni.removeStorageSync('token')
  90. uni.removeStorageSync('role')
  91. user.value = {}
  92. uni.reLaunch({ url: '/pages/public/login/index' })
  93. isFetching.value = false
  94. return
  95. }
  96. if (resp && resp.code === 200 && resp.data) {
  97. user.value = resp.data
  98. uni.setStorageSync('user_info', resp.data)
  99. // 如果缺少必要信息,引导用户补全
  100. if (!resp.data.nickname || !resp.data.avatar) {
  101. uni.navigateTo({ url: '/pages/patient/profile/infos/base-info' })
  102. }
  103. }
  104. } catch (err) {
  105. uni.hideLoading()
  106. console.error('Fetch user info error:', err)
  107. } finally {
  108. isFetching.value = false
  109. }
  110. }
  111. // 手动刷新:先从后端拉取最新用户信息,再重新生成二维码
  112. const refreshAndGenerate = async () => {
  113. await fetchUserInfo()
  114. // fetchUserInfo 可能会在 token 无效时跳转登录,下面仅在本地有用户信息时重绘二维码
  115. if (user.value && (user.value.wx_openid || user.value.openid)) {
  116. generateQRCode()
  117. } else {
  118. // 如果后端未返回 openid 等,仍尝试使用本地缓存绘制
  119. loadUser()
  120. generateQRCode()
  121. }
  122. }
  123. const generateQRCode = async () => {
  124. console.log('Generating QR code, user data:', user.value)
  125. if (!user.value.wx_openid || !user.value.role) {
  126. console.log('Missing wx_openid or role:', { wx_openid: user.value.wx_openid, role: user.value.role })
  127. uni.showToast({ title: '用户信息不完整', icon: 'none' })
  128. return
  129. }
  130. const data = JSON.stringify({
  131. openid: user.value.wx_openid,
  132. role: user.value.role
  133. })
  134. console.log('QR data:', data)
  135. qrData.value = data
  136. // 获取屏幕宽度并计算 rpx->px
  137. const width = await getWindowWidth().catch(() => 375)
  138. const qrSizePx = rpxToPx(350, width)
  139. // 使用 weapp-qrcode 生成二维码
  140. drawQrcode({
  141. width: qrSizePx,
  142. height: qrSizePx,
  143. canvasId: 'qrcode',
  144. text: data
  145. })
  146. }
  147. onShow(() => {
  148. // 首次进入时不主动向后端请求数据(首页已获取),但需要检查登录态
  149. const token = uni.getStorageSync('token')
  150. if (!token) {
  151. uni.reLaunch({ url: '/pages/public/login/index' })
  152. return
  153. }
  154. loadUser()
  155. generateQRCode()
  156. })
  157. </script>
  158. <style>
  159. .page-container {
  160. min-height: 100vh;
  161. padding-top: calc(var(--status-bar-height) + 44px);
  162. box-sizing: border-box;
  163. display: flex;
  164. justify-content: center;
  165. align-items: center;
  166. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  167. }
  168. .content {
  169. width: 100%;
  170. display: flex;
  171. justify-content: center;
  172. align-items: center;
  173. }
  174. .qr-card {
  175. background: rgba(255, 255, 255, 0.95);
  176. backdrop-filter: blur(20px);
  177. border-radius: 32rpx;
  178. padding: 60rpx 40rpx;
  179. box-shadow:
  180. 0 20rpx 40rpx rgba(0, 0, 0, 0.1),
  181. 0 8rpx 16rpx rgba(0, 0, 0, 0.05),
  182. inset 0 1rpx 0 rgba(255, 255, 255, 0.6);
  183. display: flex;
  184. flex-direction: column;
  185. align-items: center;
  186. max-width: 600rpx;
  187. width: 90%;
  188. position: relative;
  189. overflow: hidden;
  190. border: 1rpx solid rgba(255, 255, 255, 0.2);
  191. }
  192. /* 渐变边框效果 */
  193. .qr-card::after {
  194. content: '';
  195. position: absolute;
  196. top: 0;
  197. left: 0;
  198. right: 0;
  199. bottom: 0;
  200. border-radius: 32rpx;
  201. padding: 2rpx;
  202. background: linear-gradient(135deg, #667eea, #764ba2, #f093fb);
  203. -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  204. mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  205. -webkit-mask-composite: xor;
  206. mask-composite: exclude;
  207. pointer-events: none;
  208. }
  209. .user-info {
  210. text-align: center;
  211. margin-bottom: 60rpx;
  212. position: relative;
  213. width: 100%;
  214. }
  215. .title-row {
  216. display: flex;
  217. justify-content: space-between;
  218. align-items: center;
  219. margin-bottom: 30rpx;
  220. background: linear-gradient(135deg, #f8f9fa, #e9ecef);
  221. padding: 20rpx 30rpx;
  222. border-radius: 20rpx;
  223. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
  224. }
  225. .card-title {
  226. font-size: 36rpx;
  227. font-weight: 700;
  228. color: #2c3e50;
  229. text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
  230. }
  231. .role-badge {
  232. background: linear-gradient(135deg, #667eea, #764ba2);
  233. color: white;
  234. padding: 12rpx 20rpx;
  235. border-radius: 20rpx;
  236. font-size: 24rpx;
  237. font-weight: 600;
  238. box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
  239. border: 1rpx solid rgba(255, 255, 255, 0.2);
  240. }
  241. .user-name {
  242. font-size: 48rpx;
  243. font-weight: 700;
  244. color: #2c3e50;
  245. display: block;
  246. margin: 20rpx 0;
  247. background: linear-gradient(135deg, #2c3e50, #4a6572);
  248. -webkit-background-clip: text;
  249. background-clip: text;
  250. -webkit-text-fill-color: transparent;
  251. text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
  252. }
  253. .qr-desc {
  254. font-size: 28rpx;
  255. color: #6c757d;
  256. font-weight: 400;
  257. display: block;
  258. margin-top: 15rpx;
  259. line-height: 1.5;
  260. padding: 0 20rpx;
  261. }
  262. .qr-container {
  263. display: flex;
  264. justify-content: center;
  265. align-items: center;
  266. padding: 40rpx;
  267. background: linear-gradient(135deg, #f8f9fa, #e9ecef);
  268. border-radius: 24rpx;
  269. border: 1rpx solid rgba(255, 255, 255, 0.5);
  270. box-shadow:
  271. inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8),
  272. 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
  273. position: relative;
  274. overflow: hidden;
  275. }
  276. /* 二维码装饰角 */
  277. .qr-container::before,
  278. .qr-container::after {
  279. content: '';
  280. position: absolute;
  281. width: 40rpx;
  282. height: 40rpx;
  283. border: 4rpx solid #667eea;
  284. }
  285. .qr-container::before {
  286. top: 10rpx;
  287. left: 10rpx;
  288. border-right: none;
  289. border-bottom: none;
  290. }
  291. .qr-container::after {
  292. bottom: 10rpx;
  293. right: 10rpx;
  294. border-left: none;
  295. border-top: none;
  296. }
  297. .qr-container:active {
  298. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
  299. }
  300. .qr-canvas {
  301. opacity: 0.75;
  302. border-radius: 12rpx;
  303. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
  304. border: 1rpx solid rgba(0, 0, 0, 0.1);
  305. }
  306. @keyframes cardEntrance {
  307. 0% {
  308. opacity: 0;
  309. transform: translate3d(0, 30rpx, 0) scale(0.95);
  310. }
  311. 100% {
  312. opacity: 1;
  313. transform: translate3d(0, 0, 0) scale(1);
  314. }
  315. }
  316. /* 响应式优化 */
  317. /* 针对不同屏幕尺寸的适配 */
  318. @media (max-width: 375px) {
  319. .qr-card {
  320. padding: 40rpx 30rpx;
  321. width: 95%;
  322. }
  323. .user-name {
  324. font-size: 40rpx;
  325. }
  326. .qr-canvas {
  327. width: 350rpx;
  328. height: 350rpx;
  329. }
  330. }
  331. /* 深色模式支持 */
  332. @media (prefers-color-scheme: dark) {
  333. .qr-card {
  334. background: rgba(40, 40, 40, 0.95);
  335. color: #ffffff;
  336. }
  337. .card-title, .user-name {
  338. color: #ffffff;
  339. }
  340. .qr-desc {
  341. color: #b0b0b0;
  342. }
  343. }
  344. /* 添加刷新按钮(可选) */
  345. .refresh-btn {
  346. margin-top: 40rpx;
  347. padding: 20rpx 40rpx;
  348. background: linear-gradient(135deg, #667eea, #764ba2);
  349. color: white;
  350. border-radius: 25rpx;
  351. font-size: 28rpx;
  352. font-weight: 500;
  353. box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
  354. }
  355. .refresh-btn:active {
  356. box-shadow: 0 2rpx 6rpx rgba(102, 126, 234, 0.4);
  357. }
  358. </style>