profile.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. <template>
  2. <view class="container">
  3. <!-- Tabbar 切换过渡页 -->
  4. <tab-transition v-if="showTabTransition" dimCode="wealth" ref="tabTransition" @close="showTabTransition = false" />
  5. <!-- ===== 未登录状态 ===== -->
  6. <view v-if="!isLoggedIn" class="login-prompt">
  7. <view class="prompt-icon">👤</view>
  8. <text class="prompt-title">登录浠艾福</text>
  9. <text class="prompt-desc">登录后可查看个人资料、积分记录等</text>
  10. <button class="login-btn" @click="goLogin">登录 / 注册</button>
  11. </view>
  12. <!-- ===== 已登录状态 ===== -->
  13. <template v-else>
  14. <!-- 用户卡片 -->
  15. <ProfileHeader @avatar-click="goToEditInfo" />
  16. <!-- 个人信息完善卡片(全部填满后隐藏) -->
  17. <view class="profile-card" v-if="isLoggedIn && profileProgress < 14" @click="goToEditInfo">
  18. <view class="profile-card-left">
  19. <text class="profile-card-title">个人信息</text>
  20. <text class="profile-card-desc">已完善 {{ profileProgress }}/14 个字段,点击继续补充</text>
  21. </view>
  22. <text class="profile-card-arrow">›</text>
  23. </view>
  24. <!-- 成长报告 -->
  25. <ProfileGrowth />
  26. <!-- 成就徽章 -->
  27. <ProfileBadges />
  28. <!-- 健康等级称号(P2-1) -->
  29. <HealthBadge
  30. v-if="levelInfo"
  31. :levelInfo="levelInfo"
  32. :score="score"
  33. :progress="progress"
  34. :nextLevel="nextLevel"
  35. :streakDays="streakDays" />
  36. <!-- 健康故事时间线(P2-1) -->
  37. <HealthTimeline :events="timelineEvents" />
  38. <!-- 菜单列表(仅个人信息 + 设置,不含服务/商城板块) -->
  39. <ProfileMenu :role="role" :show-services="false" @invite-generate="onInviteGenerate" />
  40. </template>
  41. <AIFloatingAvatar />
  42. </view>
  43. </template>
  44. <script>
  45. import TabTransition from '../../components/tab-transition.vue'
  46. import ProfileHeader from './components/ProfileHeader.vue'
  47. import ProfileBadges from './components/ProfileBadges.vue'
  48. import ProfileGrowth from './components/ProfileGrowth.vue'
  49. import ProfileMenu from './components/ProfileMenu.vue'
  50. import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
  51. import HealthBadge from '../../components/HealthBadge.vue'
  52. import HealthTimeline from '../../components/HealthTimeline.vue'
  53. import { getHealthScoreInfo, getHealthTimeline, getUserInfo } from '../../utils/api.js'
  54. export default {
  55. components: {
  56. TabTransition,
  57. ProfileHeader,
  58. ProfileBadges,
  59. ProfileGrowth,
  60. ProfileMenu,
  61. AIFloatingAvatar,
  62. HealthBadge,
  63. HealthTimeline
  64. },
  65. data() {
  66. return {
  67. shareData: null,
  68. showTabTransition: true,
  69. selectedMascot: '',
  70. currentMascotName: '小助手',
  71. // P2-1 身份重塑
  72. levelInfo: null,
  73. score: 0,
  74. progress: 0,
  75. nextLevel: null,
  76. streakDays: 0,
  77. timelineEvents: [],
  78. // 个人信息完善进度(0-14 字段)
  79. profileProgress: 0
  80. }
  81. },
  82. computed: {
  83. isLoggedIn() {
  84. return !!uni.getStorageSync('token')
  85. },
  86. role() {
  87. return uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
  88. }
  89. },
  90. onShareAppMessage() {
  91. if (this.shareData) {
  92. return {
  93. title: this.shareData.title,
  94. path: this.shareData.path,
  95. imageUrl: '/static/invite-card.png'
  96. }
  97. }
  98. },
  99. onShow() {
  100. this.showTabTransition = true
  101. if (!uni.getStorageSync('token')) {
  102. this._watchPageReady()
  103. return
  104. }
  105. this.loadMascot()
  106. const currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
  107. if (currentRole === 'teacher') {
  108. uni.redirectTo({ url: '/pages/teacher/teacher-profile' })
  109. }
  110. this.loadHealthScoreData()
  111. this.loadProfileProgress()
  112. this._watchPageReady()
  113. },
  114. methods: {
  115. goLogin() {
  116. uni.navigateTo({ url: '/pages/login/login' })
  117. },
  118. onInviteGenerate(shareData) {
  119. this.shareData = shareData
  120. },
  121. changeMascot(mascot) {
  122. if (this.selectedMascot === mascot) return;
  123. uni.request({
  124. url: this.$config.API_BASE_URL + '/api/user/mascot/set',
  125. method: 'POST',
  126. header: { Authorization: 'Bearer ' + uni.getStorageSync('token') },
  127. data: { mascot },
  128. success: (res) => {
  129. if (res.data.code === 200 || res.data.code === 0) {
  130. this.selectedMascot = mascot;
  131. this.currentMascotName = mascot === 'xibao' ? '浠宝' : '福宝';
  132. // 同步更新 localStorage 以便 AIFloatingAvatar 等组件能读到最新值
  133. var userInfo = uni.getStorageSync('userInfo') || {};
  134. userInfo.mascot = mascot;
  135. uni.setStorageSync('userInfo', userInfo);
  136. uni.showToast({ title: '已更换' });
  137. }
  138. }
  139. });
  140. },
  141. loadMascot() {
  142. var userInfo = uni.getStorageSync('userInfo');
  143. if (userInfo && userInfo.mascot) {
  144. this.selectedMascot = userInfo.mascot;
  145. this.currentMascotName = userInfo.mascot === 'xibao' ? '浠宝' : '福宝';
  146. }
  147. },
  148. _watchPageReady() {
  149. var self = this
  150. this.$nextTick(function() {
  151. if (self.$refs.tabTransition) self.$refs.tabTransition.markReady()
  152. })
  153. },
  154. loadHealthScoreData() {
  155. var memberId = uni.getStorageSync('currentChildId') || null
  156. if (!memberId) return
  157. var self = this
  158. getHealthScoreInfo({ memberId: memberId }).then(function(res) {
  159. if (res.code === 200 && res.data) {
  160. self.levelInfo = res.data.level || null
  161. self.score = res.data.score || 0
  162. self.progress = res.data.progress || 0
  163. self.nextLevel = res.data.nextLevel || null
  164. self.streakDays = res.data.streakDays || 0
  165. }
  166. }).catch(function() {})
  167. getHealthTimeline({ memberId: memberId }).then(function(res) {
  168. if (res.code === 200 && Array.isArray(res.data)) {
  169. self.timelineEvents = res.data.slice(0, 10)
  170. }
  171. }).catch(function() {})
  172. },
  173. loadProfileProgress() {
  174. var self = this
  175. getUserInfo().then(function(res) {
  176. if (res && res.code === 200 && res.data) {
  177. var user = res.data
  178. var fields = ['nickname', 'avatar', 'realName', 'gender', 'birthday', 'birthHour', 'ethnicity', 'bloodType', 'highestEducation', 'maritalStatus', 'address', 'hobbies', 'dietPreferences', 'phone']
  179. var filled = 0
  180. for (var i = 0; i < fields.length; i++) {
  181. var v = user[fields[i]]
  182. if (v !== null && v !== undefined && v !== '') filled++
  183. }
  184. self.profileProgress = filled
  185. }
  186. }).catch(function() {})
  187. },
  188. // 修复:ProfileHeader 头像点击进入编辑页(原方法缺失)
  189. goToEditInfo() {
  190. uni.navigateTo({ url: '/pages/user-edit/user-edit' })
  191. }
  192. }
  193. }
  194. </script>
  195. <style scoped>
  196. .container {
  197. min-height: 100vh;
  198. background: #f5f7fa;
  199. padding-bottom: 120rpx;
  200. }
  201. .login-prompt {
  202. display: flex;
  203. flex-direction: column;
  204. align-items: center;
  205. justify-content: center;
  206. padding: 200rpx 60rpx;
  207. }
  208. .prompt-icon { font-size: 100rpx; margin-bottom: 30rpx; }
  209. .prompt-title { font-size: 40rpx; font-weight: bold; color: #333; margin-bottom: 16rpx; }
  210. .prompt-desc { font-size: 26rpx; color: #999; margin-bottom: 60rpx; }
  211. .login-btn {
  212. width: 80%;
  213. background: linear-gradient(135deg, #667eea, #764ba2);
  214. color: #fff;
  215. border-radius: 50rpx;
  216. font-size: 30rpx;
  217. padding: 24rpx;
  218. line-height: 1.5;
  219. }
  220. .mascot-selector {
  221. margin: 20rpx 0;
  222. padding: 24rpx;
  223. background: #fff;
  224. border-radius: 16rpx;
  225. }
  226. .selector-header {
  227. margin-bottom: 20rpx;
  228. }
  229. .selector-title {
  230. font-size: 32rpx;
  231. font-weight: bold;
  232. color: #1F2937;
  233. }
  234. .selector-subtitle {
  235. font-size: 24rpx;
  236. color: #6366F1;
  237. margin-left: 16rpx;
  238. }
  239. .mascot-cards {
  240. display: flex;
  241. gap: 20rpx;
  242. }
  243. .mascot-card {
  244. flex: 1;
  245. display: flex;
  246. flex-direction: column;
  247. align-items: center;
  248. padding: 24rpx 0;
  249. border: 2rpx solid #E5E7EB;
  250. border-radius: 12rpx;
  251. }
  252. .mascot-card.active {
  253. border-color: #6366F1;
  254. background: #EEF2FF;
  255. }
  256. .card-icon {
  257. width: 80rpx;
  258. height: 80rpx;
  259. border-radius: 50%;
  260. display: flex;
  261. align-items: center;
  262. justify-content: center;
  263. font-size: 32rpx;
  264. font-weight: bold;
  265. color: #fff;
  266. margin-bottom: 12rpx;
  267. }
  268. .card-icon.xibao {
  269. background: linear-gradient(135deg, #FF8C42, #FF6B9D);
  270. }
  271. .card-icon.fubao {
  272. background: linear-gradient(135deg, #6366F1, #10B981);
  273. }
  274. .card-name {
  275. font-size: 26rpx;
  276. color: #374151;
  277. }
  278. .profile-card {
  279. display: flex;
  280. flex-direction: row;
  281. align-items: center;
  282. justify-content: space-between;
  283. margin: 20rpx 30rpx;
  284. padding: 28rpx 32rpx;
  285. background: #fff;
  286. border-radius: 16rpx;
  287. }
  288. .profile-card-left {
  289. display: flex;
  290. flex-direction: column;
  291. }
  292. .profile-card-title {
  293. font-size: 30rpx;
  294. font-weight: bold;
  295. color: #333;
  296. }
  297. .profile-card-desc {
  298. font-size: 24rpx;
  299. color: #999;
  300. margin-top: 8rpx;
  301. }
  302. .profile-card-arrow {
  303. font-size: 40rpx;
  304. color: #ccc;
  305. }
  306. </style>