profile.vue 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503
  1. <template>
  2. <view class="container">
  3. <!-- 未登录 -->
  4. <view v-if="!isLoggedIn">
  5. <view class="login-prompt">
  6. <text class="prompt-icon">👤</text>
  7. <text class="prompt-title">登录浠艾福</text>
  8. <text class="prompt-desc">登录后查看个人资料、积分记录、能量数据与家庭管理</text>
  9. <button class="login-btn" @click="goLogin">登录 / 注册</button>
  10. <text class="skip-link" @click="goSkip">暂不登录,先逛逛 ›</text>
  11. </view>
  12. <view class="philosophy-card">
  13. <text class="philosophy-title">照顾家庭,从照顾好自己开始</text>
  14. <text class="philosophy-desc">你的状态,就是家的状态。先安顿好自己,才有余力温暖家人。</text>
  15. </view>
  16. </view>
  17. <!-- 已登录 -->
  18. <template v-else>
  19. <!-- 退出切换 -->
  20. <view class="exit-switch" v-if="isSwitchedChild" @click="onExitSwitch">
  21. <text>🔄 退出切换</text>
  22. </view>
  23. <!-- 身份卡 -->
  24. <view class="user-card">
  25. <view class="user-card-left">
  26. <view class="avatar-wrap" @click="goToEditInfo">
  27. <image v-if="userInfo.avatar" class="avatar-img" :src="userInfo.avatar" mode="aspectFill" />
  28. <text v-else class="avatar-text">{{ nickname.charAt(0) || '👤' }}</text>
  29. </view>
  30. <view class="user-meta">
  31. <view class="user-name-row">
  32. <text class="nickname">{{ nickname || '未设置昵称' }}</text>
  33. <text v-if="roleLabel" class="role-tag">{{ roleLabel }}</text>
  34. </view>
  35. <view class="user-sub" v-if="memberLevel">
  36. <view class="level-badge" :class="'level-' + memberLevel">{{ memberLevelText }}</view>
  37. <text v-if="isLifetime" class="days-left">永久有效</text>
  38. <text v-else-if="membershipDays > 0 && membershipDays <= 30" class="days-left">剩余 {{ membershipDays }} 天</text>
  39. <view v-if="hasUpgradeSpace" class="upgrade-btn" @click="goUpgrade">
  40. <text class="upgrade-btn-text">{{ upgradeBtnText }}</text>
  41. </view>
  42. </view>
  43. <view class="user-sub" v-else>
  44. <text class="days-left">开通会员解锁更多权益</text>
  45. <view class="upgrade-btn" @click="goUpgrade">
  46. <text class="upgrade-btn-text">立即开通</text>
  47. </view>
  48. </view>
  49. </view>
  50. </view>
  51. <view class="user-card-right" @click="goToPoints">
  52. <text class="points-num">{{ totalPoints }}</text>
  53. <text class="points-label">积分</text>
  54. </view>
  55. </view>
  56. <!-- 我的分享海报浮动按钮(纯 CSS 二维码图标,避免 emoji 渲染成计算器) -->
  57. <view class="share-qr-float" @click="showShareQr">
  58. <view class="qr-glyph">
  59. <view class="qr-eye qr-eye-tl"></view>
  60. <view class="qr-eye qr-eye-tr"></view>
  61. <view class="qr-eye qr-eye-bl"></view>
  62. <view class="qr-mid"></view>
  63. <view class="qr-cluster">
  64. <view class="qr-pip"></view>
  65. <view class="qr-pip"></view>
  66. <view class="qr-pip"></view>
  67. <view class="qr-pip"></view>
  68. </view>
  69. </view>
  70. </view>
  71. <!-- 家族成员速览 -->
  72. <view class="family-strip" v-if="familyMembers.length > 0">
  73. <view class="family-strip-header">
  74. <text class="family-strip-title">我的家庭</text>
  75. <text class="family-strip-count">{{ familyMembers.length }} 位成员</text>
  76. </view>
  77. <view class="family-avatars">
  78. <view
  79. v-for="member in familyMembers"
  80. :key="member.id"
  81. class="family-avatar-item"
  82. @click="goMemberDetail(member)"
  83. >
  84. <view class="family-avatar-wrap">
  85. <image v-if="member.avatar" class="family-avatar-img" :src="member.avatar" mode="aspectFill" />
  86. <text v-else class="family-avatar-text">{{ member.nickname ? member.nickname.charAt(0) : '?' }}</text>
  87. <view class="family-avatar-tag" :class="'tag-' + (member.effectiveRole || member.source || 'member')">{{ member.roleLabel || memberRoleLabel(member.effectiveRole) }}</view>
  88. </view>
  89. <text class="family-avatar-name">{{ member.nickname || member.realName || '成员' }}</text>
  90. </view>
  91. <view class="family-avatar-item" @click="goToFamily">
  92. <view class="family-avatar-wrap family-avatar-add">
  93. <text class="family-avatar-plus">+</text>
  94. </view>
  95. <text class="family-avatar-name">添加</text>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 用户画像 - 五维能量概览 -->
  100. <view class="portrait-section" v-if="portrait">
  101. <view class="section-header-row">
  102. <text class="section-title">🧭 用户画像</text>
  103. <text class="section-sub">{{ portrait.tierLabel }}</text>
  104. </view>
  105. <view class="portrait-summary">
  106. <text class="portrait-desc">{{ portrait.summary }}</text>
  107. </view>
  108. <view class="portrait-dimensions">
  109. <view class="portrait-dim-item" v-for="dim in portrait.dims" :key="dim.code">
  110. <view class="portrait-dim-header">
  111. <view class="portrait-dim-icon" :style="{ background: dim.color }">{{ dim.icon }}</view>
  112. <text class="portrait-dim-name">{{ dim.name }}</text>
  113. <text class="portrait-dim-score">{{ dim.score }}</text>
  114. </view>
  115. <view class="portrait-dim-bar-track">
  116. <view class="portrait-dim-bar-fill" :style="{ width: dim.score + '%', background: dim.color }"></view>
  117. </view>
  118. <text class="portrait-dim-desc">{{ dim.desc }}</text>
  119. </view>
  120. </view>
  121. <view class="portrait-tip" v-if="portrait.tip">
  122. <text class="portrait-tip-text">{{ portrait.tip }}</text>
  123. </view>
  124. </view>
  125. <!-- 个人成就统计 -->
  126. <view class="achievement-section">
  127. <view class="section-header-row">
  128. <text class="section-title">🏆 我的成就</text>
  129. <text class="section-sub">持续成长,看见改变</text>
  130. </view>
  131. <view class="achievement-grid">
  132. <view class="ach-item" @click="goToPoints">
  133. <view class="ach-icon-wrap" style="background:rgba(249,115,22,0.12)">
  134. <text class="ach-icon">💰</text>
  135. </view>
  136. <text class="ach-num">{{ totalPoints }}</text>
  137. <text class="ach-label">累计积分</text>
  138. </view>
  139. <view class="ach-item" @click="goToStreak">
  140. <view class="ach-icon-wrap" style="background:rgba(255,140,66,0.12)">
  141. <text class="ach-icon">🔥</text>
  142. </view>
  143. <text class="ach-num">{{ streakDays }}</text>
  144. <text class="ach-label">连续打卡</text>
  145. </view>
  146. <view class="ach-item" @click="goToTasks">
  147. <view class="ach-icon-wrap" style="background:rgba(99,102,241,0.12)">
  148. <text class="ach-icon">✅</text>
  149. </view>
  150. <text class="ach-num">{{ completedTasks }}</text>
  151. <text class="ach-label">完成任务</text>
  152. </view>
  153. <view class="ach-item" @click="goToWishes">
  154. <view class="ach-icon-wrap" style="background:rgba(245,158,11,0.12)">
  155. <text class="ach-icon">🎁</text>
  156. </view>
  157. <text class="ach-num">{{ rewardsEarned }}</text>
  158. <text class="ach-label">获得奖励</text>
  159. </view>
  160. <view class="ach-item" @click="goToBadges">
  161. <view class="ach-icon-wrap" style="background:rgba(236,72,153,0.12)">
  162. <text class="ach-icon">🏅</text>
  163. </view>
  164. <text class="ach-num">{{ badgesCollected }}</text>
  165. <text class="ach-label">收集勋章</text>
  166. </view>
  167. <view class="ach-item" @click="goToGrowth">
  168. <view class="ach-icon-wrap" style="background:rgba(16,185,129,0.12)">
  169. <text class="ach-icon">📈</text>
  170. </view>
  171. <text class="ach-num">{{ growthRecords }}</text>
  172. <text class="ach-label">成长记录</text>
  173. </view>
  174. <view class="ach-item" @click="goToChallenges">
  175. <view class="ach-icon-wrap" style="background:rgba(255,107,157,0.12)">
  176. <text class="ach-icon">🎯</text>
  177. </view>
  178. <text class="ach-num">{{ challengesJoined }}</text>
  179. <text class="ach-label">参加挑战</text>
  180. </view>
  181. <view class="ach-item" @click="goToHealthReports">
  182. <view class="ach-icon-wrap" style="background:rgba(14,165,233,0.12)">
  183. <text class="ach-icon">📋</text>
  184. </view>
  185. <text class="ach-num">{{ healthReports }}</text>
  186. <text class="ach-label">上传报告</text>
  187. </view>
  188. <view class="ach-item" @click="goToWealthCenter">
  189. <view class="ach-icon-wrap" style="background:rgba(245,158,11,0.12)">
  190. <text class="ach-icon">💰</text>
  191. </view>
  192. <text class="ach-num">{{ referralEarnings }}</text>
  193. <text class="ach-label">推广收益</text>
  194. </view>
  195. <view class="ach-item" @click="goTeamTree">
  196. <view class="ach-icon-wrap" style="background:rgba(16,185,129,0.12)">
  197. <text class="ach-icon">🌳</text>
  198. </view>
  199. <text class="ach-num">{{ teamMembers }}</text>
  200. <text class="ach-label">我的团队</text>
  201. </view>
  202. </view>
  203. </view>
  204. <!-- 我的订单(电商风格) -->
  205. <view class="order-section">
  206. <view class="order-section-header">
  207. <text class="section-title">🛒 我的订单</text>
  208. <text class="order-all-link" @click="goToOrders">全部订单 ›</text>
  209. </view>
  210. <view class="order-card">
  211. <view class="order-grid">
  212. <view class="order-grid-item" @click="goToOrderStatus('pending')">
  213. <view class="order-icon-wrap">
  214. <text class="order-icon">💳</text>
  215. <text class="order-badge" v-if="orderPending > 0">{{ orderPending }}</text>
  216. </view>
  217. <text class="order-grid-text">待付款</text>
  218. </view>
  219. <view class="order-grid-item" @click="goToOrderStatus('paid')">
  220. <view class="order-icon-wrap">
  221. <text class="order-icon">📦</text>
  222. <text class="order-badge" v-if="orderUnpaid > 0">{{ orderUnpaid }}</text>
  223. </view>
  224. <text class="order-grid-text">待发货</text>
  225. </view>
  226. <view class="order-grid-item" @click="goToOrderStatus('shipped')">
  227. <view class="order-icon-wrap">
  228. <text class="order-icon">🚚</text>
  229. <text class="order-badge" v-if="orderShipped > 0">{{ orderShipped }}</text>
  230. </view>
  231. <text class="order-grid-text">待收货</text>
  232. </view>
  233. <view class="order-grid-item" @click="goToOrderStatus('refunding')">
  234. <view class="order-icon-wrap">
  235. <text class="order-icon">🔧</text>
  236. <text class="order-badge" v-if="orderAfterSale > 0">{{ orderAfterSale }}</text>
  237. </view>
  238. <text class="order-grid-text">售后</text>
  239. </view>
  240. <view class="order-grid-item" @click="goToOrders">
  241. <view class="order-icon-wrap">
  242. <text class="order-icon">📋</text>
  243. </view>
  244. <text class="order-grid-text">全部</text>
  245. </view>
  246. </view>
  247. <view class="order-extra-row">
  248. <view class="order-extra-item" @click="goToCart">
  249. <text class="order-extra-icon">🛒</text>
  250. <text class="order-extra-text">购物车</text>
  251. <text class="order-extra-badge" v-if="cartCount > 0">{{ cartCount > 99 ? '99+' : cartCount }}</text>
  252. </view>
  253. <view class="order-extra-divider"></view>
  254. <view class="order-extra-item" @click="goToAddress">
  255. <text class="order-extra-icon">📍</text>
  256. <text class="order-extra-text">收货地址</text>
  257. </view>
  258. </view>
  259. </view>
  260. </view>
  261. <!-- 规划师服务中心入口(仅规划师可见) -->
  262. <view class="service-center-section" v-if="role === 'teacher' && teacherStatus === 'approved'">
  263. <view class="service-center-card" @click="goToServiceCenter">
  264. <view class="service-center-icon">👨‍🏫</view>
  265. <view class="service-center-info">
  266. <text class="service-center-title">服务中心</text>
  267. <text class="service-center-desc">待处理任务:{{ pendingTasks }} 条 · 消息:{{ unreadMessages }} 条</text>
  268. </view>
  269. <text class="service-center-arrow">›</text>
  270. </view>
  271. </view>
  272. <!-- 规划师待审核提示 -->
  273. <view class="service-center-section" v-if="role === 'teacher' && teacherStatus !== 'approved'">
  274. <view class="service-center-card disabled-card">
  275. <view class="service-center-icon">👨‍🏫</view>
  276. <view class="service-center-info">
  277. <text class="service-center-title">服务中心</text>
  278. <text class="service-center-desc">{{ teacherStatus === 'pending' ? '申请审核中,暂未开放' : teacherStatus === 'rejected' ? '申请未通过,请联系管理员' : '规划师服务' }}</text>
  279. </view>
  280. </view>
  281. </view>
  282. <!-- 设置 + 退出 -->
  283. <view class="bottom-menu-section">
  284. <view class="menu-row" @click="goToSettings">
  285. <text class="menu-row-icon">⚙️</text>
  286. <text class="menu-row-text">设置</text>
  287. <text class="menu-row-arrow">›</text>
  288. </view>
  289. <view class="logout-btn" @click="logout">
  290. <text class="logout-text">退出登录</text>
  291. </view>
  292. </view>
  293. </template>
  294. <!-- 我的分享海报:复用文章/活动/商品同一套 ContentSharePoster 组件(canvas 绘制 + 保存到相册) -->
  295. <ContentSharePoster
  296. :show="showSharePoster"
  297. :title="shareHeadline"
  298. :energyData="posterEnergyData"
  299. :brandFallback="true"
  300. :qrCodeBase64="posterQrCode || ''"
  301. typeLabel="邀友加入"
  302. :summary="posterSummary"
  303. ctaText="长按识别二维码 · 加入浠艾福"
  304. :inviteText="posterInviteText"
  305. @close="closeSharePoster"
  306. />
  307. <FloatingAvatar />
  308. </view>
  309. </template>
  310. <script>
  311. import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
  312. import ContentSharePoster from '../../components/ContentSharePoster.vue'
  313. import {
  314. getChildren, getFamilyMemberList,
  315. getTodayTasksByCategory, getChallengeList, getMyMembership,
  316. verifyPassword, getPointsBalance, getUserInfo,
  317. cartCount, getOrderCounts, getReferralSummary,
  318. getProfileStats, getMyReferrer, getTeamConsumption,
  319. getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount,
  320. getReferralCode, extractReferralCode, getShareQrCode, getEnergySandbox
  321. } from '../../utils/api.js'
  322. export default {
  323. components: { FloatingAvatar, ContentSharePoster },
  324. data() {
  325. return {
  326. nickname: '',
  327. role: 'parent',
  328. isSwitchedChild: false,
  329. userInfo: {},
  330. memberLevel: null,
  331. membershipDays: 0,
  332. totalPoints: 0,
  333. referralEarnings: 0,
  334. teamMembers: 0,
  335. // 个人成就
  336. completedTasks: 0,
  337. streakDays: 0,
  338. rewardsEarned: 0,
  339. badgesCollected: 0,
  340. growthRecords: 0,
  341. challengesJoined: 0,
  342. healthReports: 0,
  343. // 家庭
  344. familyMembers: [],
  345. // 商城
  346. orderPending: 0,
  347. orderUnpaid: 0,
  348. orderShipped: 0,
  349. orderAfterSale: 0,
  350. cartCount: 0,
  351. // 待办
  352. pendingTasks: 0,
  353. unreadMessages: 0,
  354. teacherStatus: 'pending',
  355. profileProgress: 0,
  356. // 五维能量
  357. dimensionScores: { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 },
  358. portrait: null,
  359. myReferrer: null,
  360. // 分享推广码
  361. showSharePoster: false,
  362. referralCode: '',
  363. posterQrCode: '',
  364. posterEnergyData: null,
  365. referralCodeLoaded: false,
  366. }
  367. },
  368. computed: {
  369. isLoggedIn() {
  370. return !!uni.getStorageSync('token')
  371. },
  372. roleLabel() {
  373. var map = { parent: '', child: '成员', teacher: '规划师' }
  374. return map[this.role] || '用户'
  375. },
  376. memberLevelText() {
  377. var map = { FREE: '免费用户', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
  378. return map[this.memberLevel] || this.memberLevel || ''
  379. },
  380. isLifetime() {
  381. return this.memberLevel === 'LIFETIME'
  382. },
  383. hasUpgradeSpace() {
  384. return this.memberLevel === 'FREE' || this.memberLevel === 'FAMILY'
  385. },
  386. upgradeBtnText() {
  387. return this.hasUpgradeSpace ? '升级' : '立即开通'
  388. },
  389. // 分享海报标题:XXX邀请你关注家庭健康(昵称过长自动截断)
  390. shareHeadline() {
  391. var name = (this.nickname || '').trim()
  392. if (!name) return '邀请你关注家庭健康'
  393. if (name.length > 8) name = name.substring(0, 8) + '…'
  394. return name + '邀请你关注家庭健康'
  395. },
  396. posterSummary() {
  397. return '身体 · 心理 · 智慧 · 行动 · 财富,一家专注全家健康的成长平台'
  398. },
  399. posterInviteText() {
  400. var name = (this.nickname || '').trim()
  401. if (!name) return '扫码加入浠艾福,一起照顾好这个家'
  402. if (name.length > 8) name = name.substring(0, 8) + '…'
  403. return '「' + name + '」邀请你加入浠艾福'
  404. }
  405. },
  406. onShow() {
  407. if (!this.isLoggedIn) {
  408. return
  409. }
  410. var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
  411. this.role = role
  412. this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
  413. this.nickname = (uni.getStorageSync('userInfo') || {}).nickname || ''
  414. // 规划师加载额外数据
  415. if (role === 'teacher') {
  416. this.loadTeacherStatus()
  417. this.loadTeacherData()
  418. } else {
  419. this.loadAllData()
  420. this.loadMyReferrer()
  421. }
  422. },
  423. methods: {
  424. loadTeacherStatus() {
  425. var self = this
  426. getTeacherInfo().then(function(res) {
  427. if (res && res.code === 200 && res.data) {
  428. self.teacherStatus = res.data.verificationStatus || res.data.status || 'pending'
  429. }
  430. }).catch(function() {})
  431. },
  432. loadTeacherData() {
  433. var self = this
  434. // 并行加载待处理任务和未读消息
  435. getTeacherPendingTasks().then(function(res) {
  436. if (res && res.data && Array.isArray(res.data)) {
  437. self.pendingTasks = res.data.length
  438. } else if (res && res.data && res.data.pendingCount != null) {
  439. self.pendingTasks = res.data.pendingCount
  440. }
  441. }).catch(function() {})
  442. getTeacherUnreadCount().then(function(res) {
  443. if (res && res.data != null) {
  444. self.unreadMessages = res.data
  445. }
  446. }).catch(function() {})
  447. },
  448. loadAllData() {
  449. this.loadUserInfo()
  450. this.loadMembership()
  451. this.loadPoints()
  452. this.loadFamily()
  453. this.loadTasks()
  454. this.loadChallenges()
  455. this.loadOrderCounts()
  456. this.loadCartCount()
  457. this.loadProfileStats()
  458. this.loadReferralEarnings()
  459. this.loadTeamInfo()
  460. this.loadDimensionEnergy()
  461. },
  462. loadUserInfo() {
  463. var self = this
  464. getUserInfo().then(function(res) {
  465. if (res && res.code === 200 && res.data) {
  466. self.userInfo = res.data
  467. if (!self.nickname) self.nickname = res.data.nickname || ''
  468. }
  469. }).catch(function() {})
  470. },
  471. loadMembership() {
  472. var self = this
  473. getMyMembership().then(function(res) {
  474. if (res && res.data) {
  475. self.memberLevel = res.data.memberLevel || res.data.levelCode || null
  476. self.membershipDays = res.data.membershipDays || res.data.daysRemaining || 0
  477. }
  478. }).catch(function() {})
  479. },
  480. loadPoints() {
  481. // 累计积分由 loadProfileStats 汇总家庭成员积分,此处不再单独覆盖
  482. },
  483. loadFamily() {
  484. var self = this
  485. getFamilyMemberList({ visibleOnly: true }).then(function(res) {
  486. var data = res && res.data ? res.data : []
  487. self.familyMembers = Array.isArray(data) ? data : []
  488. }).catch(function() {
  489. self.familyMembers = []
  490. })
  491. },
  492. loadTasks() {
  493. var self = this
  494. var memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('userId')
  495. if (!memberId) return
  496. getTodayTasksByCategory(memberId, 'action').then(function(res) {
  497. if (res && res.data) {
  498. self.pendingTasks = Array.isArray(res.data) ? res.data.length : 0
  499. }
  500. }).catch(function() {})
  501. // fallback to storage
  502. if (!self.pendingTasks) {
  503. self.pendingTasks = parseInt(uni.getStorageSync('pendingTaskCount') || 0)
  504. }
  505. },
  506. loadChallenges() {
  507. var self = this
  508. getChallengeList({}).then(function(res) {
  509. if (res && res.data) {
  510. // 不在此处使用,仅确保请求
  511. }
  512. }).catch(function() {})
  513. },
  514. loadOrderCounts() {
  515. var self = this
  516. getOrderCounts().then(function(res) {
  517. if (res && res.data) {
  518. self.orderPending = res.data.pending || 0
  519. self.orderUnpaid = res.data.paid || 0
  520. self.orderShipped = res.data.shipped || 0
  521. self.orderAfterSale = res.data.refunding || 0
  522. }
  523. }).catch(function() {})
  524. },
  525. loadCartCount() {
  526. var self = this
  527. cartCount().then(function(res) {
  528. if (res && res.data != null) {
  529. self.cartCount = res.data || 0
  530. }
  531. }).catch(function() {})
  532. },
  533. loadProfileStats() {
  534. var self = this
  535. getProfileStats().then(function(res) {
  536. if (res && res.code === 200 && res.data) {
  537. var d = res.data
  538. if (d.totalPoints != null) self.totalPoints = d.totalPoints
  539. if (d.streakDays != null) self.streakDays = d.streakDays
  540. if (d.completedTasks != null) self.completedTasks = d.completedTasks
  541. if (d.rewardsEarned != null) self.rewardsEarned = d.rewardsEarned
  542. if (d.badgesCollected != null) self.badgesCollected = d.badgesCollected
  543. if (d.growthRecords != null) self.growthRecords = d.growthRecords
  544. if (d.challengesJoined != null) self.challengesJoined = d.challengesJoined
  545. if (d.healthReports != null) self.healthReports = d.healthReports
  546. }
  547. }).catch(function() {})
  548. },
  549. loadDimensionEnergy() {
  550. var self = this
  551. getEnergySandbox('family').then(function(res) {
  552. if (res && res.code === 200 && res.data) {
  553. var d = res.data.familyData || null
  554. if (!d) {
  555. if (res.data.personalData) {
  556. // 个人数据模式:使用个人数据
  557. d = res.data.personalData
  558. } else {
  559. self.portrait = null
  560. return
  561. }
  562. }
  563. if (!d || typeof d !== 'object') return
  564. // 设置五维能量分数
  565. self.dimensionScores = {
  566. body: d.bodyScore ?? d.body_score ?? 0,
  567. mind: d.mindScore ?? d.mind_score ?? 0,
  568. wisdom: d.wisdomScore ?? d.wisdom_score ?? 0,
  569. action: d.actionScore ?? d.action_score ?? 0,
  570. wealth: d.wealthScore ?? d.wealth_score ?? 0
  571. }
  572. // 生成用户画像
  573. self.portrait = self.mapToPortrait(d)
  574. }
  575. }).catch(function(e) {
  576. console.error('获取五维能量沙盘失败', e)
  577. self.portrait = null
  578. })
  579. },
  580. loadReferralEarnings() {
  581. var self = this
  582. getReferralSummary().then(function(res) {
  583. if (res && res.code === 200 && res.data) {
  584. self.referralEarnings = res.data.totalEarnings || 0
  585. }
  586. }).catch(function() {})
  587. },
  588. loadTeamInfo() {
  589. var self = this
  590. getTeamConsumption(3).then(function(res) {
  591. if (res && res.data && res.data.totalMembers != null) {
  592. self.teamMembers = res.data.totalMembers
  593. }
  594. }).catch(function() {})
  595. },
  596. async loadMyReferrer() {
  597. try {
  598. var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId') || 0
  599. var res = await getMyReferrer(memberId)
  600. if (res && res.code === 200 && res.data && res.data.hasReferrer) {
  601. this.myReferrer = res.data
  602. }
  603. } catch (e) {
  604. console.error('获取推荐人失败', e)
  605. }
  606. },
  607. // ===== 分享海报(复用 ContentSharePoster,与文章/活动/商品同一套生成逻辑)=====
  608. async showShareQr() {
  609. uni.showLoading({ title: '生成海报中...' })
  610. try {
  611. if (!this.referralCodeLoaded) {
  612. await this.loadReferralCode()
  613. }
  614. var scene = 'ref=' + (this.referralCode || '')
  615. if (scene.length > 32) {
  616. scene = scene.substring(0, 32)
  617. }
  618. // 首页路径,扫码后进入首页并绑定推荐人
  619. // 注意:微信 getwxacodeunlimit 的 page 不能带前导斜杠,否则报 errcode 41030 invalid page rid
  620. var res = await getShareQrCode('pages/index-home/index', scene)
  621. if (res && res.code === 200 && res.data && res.data.qrCodeBase64) {
  622. this.posterQrCode = res.data.qrCodeBase64
  623. // 必须在 showSharePoster=true 之前取到数据:组件 watch 到 show 后 300ms 即开始绘制
  624. await this.loadPosterEnergy()
  625. this.showSharePoster = true
  626. } else {
  627. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  628. }
  629. } catch (e) {
  630. console.error('生成分享海报失败', e)
  631. uni.showToast({ title: '生成失败,请重试', icon: 'none' })
  632. } finally {
  633. uni.hideLoading()
  634. }
  635. },
  636. closeSharePoster() {
  637. this.showSharePoster = false
  638. },
  639. // 五维能量数据卡数据(方案C):家庭聚合视角;未解锁全景/取数失败时置空,海报降级为品牌卡
  640. loadPosterEnergy() {
  641. var self = this
  642. return getEnergySandbox('family').then(function(res) {
  643. self.posterEnergyData = self.mapPosterEnergy(res)
  644. }).catch(function(e) {
  645. console.error('获取五维能量失败,海报降级为品牌卡', e)
  646. self.posterEnergyData = null
  647. })
  648. },
  649. mapPosterEnergy(res) {
  650. if (!res || res.code !== 200 || !res.data) return null
  651. // locked=true 表示用户未解锁全景,此时不展示任何数据,改为品牌卡
  652. if (res.data.locked) return null
  653. // 只认家庭聚合视角;仅当接口返回的是裸 DTO(无 personalData/familyData 包装)时才直接取 res.data
  654. var d = res.data.familyData || null
  655. if (!d) {
  656. if (res.data.personalData) return null
  657. d = res.data
  658. }
  659. if (!d || typeof d !== 'object') return null
  660. var defs = [
  661. { key: 'body', label: '身', color: '#FF8C42' },
  662. { key: 'mind', label: '心', color: '#FF6B9D' },
  663. { key: 'wisdom', label: '智', color: '#6366F1' },
  664. { key: 'action', label: '行', color: '#10B981' },
  665. { key: 'wealth', label: '富', color: '#F59E0B' }
  666. ]
  667. var dims = []
  668. var sum = 0
  669. for (var i = 0; i < defs.length; i++) {
  670. var raw = d[defs[i].key + 'Score']
  671. if (raw === null || raw === undefined || raw === '' || isNaN(raw)) continue
  672. var n = Number(raw)
  673. if (n < 0) n = 0
  674. if (n > 100) n = 100
  675. sum += n
  676. dims.push({ key: defs[i].key, label: defs[i].label, score: n, color: defs[i].color })
  677. }
  678. if (dims.length === 0) return null
  679. var overall = d.overallScore
  680. if (overall === null || overall === undefined || overall === '' || isNaN(overall)) {
  681. overall = Math.round(sum / dims.length)
  682. }
  683. return { label: '我们家的能量', overall: Number(overall), dims: dims }
  684. },
  685. mapToPortrait(d) {
  686. // 计算总分和平均分
  687. var scores = { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
  688. var dims = []
  689. var total = 0
  690. var count = 0
  691. var defs = [
  692. { key: 'body', label: '身', color: '#FF8C42', desc: '身体健康状况,体质好坏的综合反映' },
  693. { key: 'mind', label: '心', color: '#FF6B9D', desc: '情绪与心理状态,情绪波动与心理平衡' },
  694. { key: 'wisdom', label: '智', color: '#6366F1', desc: '智力与知识水平,认知能力与知识储备' },
  695. { key: 'action', label: '行', color: '#10B981', desc: '行动力与执行力,计划与行动的落实情况' },
  696. { key: 'wealth', label: '富', color: '#F59E0B', desc: '财富与资源,物质丰富程度与创富能力' }
  697. ]
  698. for (var i = 0; i < defs.length; i++) {
  699. var raw = d[defs[i].key + 'Score']
  700. var n = 0
  701. if (raw != null && !isNaN(raw)) {
  702. n = Math.max(0, Math.min(100, Number(raw)))
  703. }
  704. scores[defs[i].key] = n
  705. total += n
  706. count++
  707. dims.push({
  708. code: defs[i].key,
  709. name: defs[i].label,
  710. score: n,
  711. color: defs[i].color,
  712. desc: defs[i].desc
  713. })
  714. }
  715. var overall = count > 0 ? Math.round(total / count) : 0
  716. // 确定等级和描述
  717. var tierLabel = ''
  718. var summary = ''
  719. var tip = ''
  720. if (overall >= 80) {
  721. tierLabel = '卓越守护者'
  722. summary = '你的五维能量处于卓越水平,身心健康,知行合一。家庭成员都受益于你的正向能量场。'
  723. tip = '保持当前的良好习惯,继续关注家庭成员的需求'
  724. } else if (overall >= 60) {
  725. tierLabel = '进取者'
  726. summary = '你的五维能量处于良好水平,各方面有所提升,但仍有提升空间。'
  727. tip = '可以尝试在其中一个维度上突破瓶颈,带动全家进步'
  728. } else if (overall >= 40) {
  729. tierLabel = '探索者'
  730. summary = '你的五维能量处于起步水平,正在探索和学习中。这是成长的起点。'
  731. tip = '从关注最容易改进的维度开始,每一点进步都很重要'
  732. } else {
  733. tierLabel = '起步者'
  734. summary = '你的五维能量处于起始水平,正处于开始关注全家健康的阶段。'
  735. tip = '从现在开始,每天关注一项小健康行动,逐步建立五维能量基础'
  736. }
  737. return {
  738. overall: overall,
  739. tierLabel: tierLabel,
  740. summary: summary,
  741. dims: dims,
  742. tip: tip
  743. }
  744. },
  745. async loadReferralCode() {
  746. if (this.referralCodeLoaded) return
  747. var self = this
  748. try {
  749. var res = await getReferralCode()
  750. self.referralCode = extractReferralCode(res) || ''
  751. } catch (e) {
  752. console.error('获取推广码失败', e)
  753. } finally {
  754. self.referralCodeLoaded = true
  755. }
  756. },
  757. goToServiceCenter() {
  758. uni.navigateTo({ url: '/pages/service-center/index' })
  759. },
  760. goTeamTree() {
  761. uni.navigateTo({ url: '/pages/profile/team-tree' })
  762. },
  763. goLogin: function() {
  764. uni.navigateTo({ url: '/pages/login/login' })
  765. },
  766. goSkip: function() {
  767. uni.switchTab({ url: '/pages/index-home/index' })
  768. },
  769. goToEditInfo: function() {
  770. uni.navigateTo({ url: '/pages/user-edit/user-edit' })
  771. },
  772. goToPoints: function() {
  773. uni.navigateTo({ url: '/pages/points/points' })
  774. },
  775. goToStreak: function() {
  776. var memberId = uni.getStorageSync('currentMemberId') || uni.getStorageSync('currentChildId')
  777. var url = '/pages/stats/completion-rate'
  778. if (memberId) {
  779. url += '?memberId=' + memberId
  780. } else {
  781. url += '?familyView=1'
  782. }
  783. uni.navigateTo({ url: url })
  784. },
  785. goToTasks: function() {
  786. uni.navigateTo({ url: '/pages/tasks/tasks' })
  787. },
  788. goToWishes: function() {
  789. uni.navigateTo({ url: '/pages/wishes/index' })
  790. },
  791. goToBadges: function() {
  792. uni.navigateTo({ url: '/pages/rewards/badge' })
  793. },
  794. goToGrowth: function() {
  795. uni.navigateTo({ url: '/pages/growth-main/index' })
  796. },
  797. goToChallenges: function() {
  798. uni.navigateTo({ url: '/pages/health/challenge-manage' })
  799. },
  800. goToHealthReports: function() {
  801. uni.navigateTo({ url: '/pages/health/report-list' })
  802. },
  803. goToWealthCenter: function() {
  804. uni.navigateTo({ url: '/pages/promotion/index' })
  805. },
  806. goUpgrade: function() {
  807. uni.navigateTo({ url: '/pages/membership/upgrade' })
  808. },
  809. goToFamily: function() {
  810. uni.navigateTo({ url: '/pages/profile-extra/family-members' })
  811. },
  812. goMemberDetail: function(member) {
  813. uni.navigateTo({ url: '/pages/home-pages/member-home-detail?memberId=' + member.id })
  814. },
  815. goToAppointments: function() {
  816. uni.navigateTo({ url: '/pages/parent/appointments' })
  817. },
  818. goToOrders: function() {
  819. uni.navigateTo({ url: '/pages/shop/order-list/order-list' })
  820. },
  821. goToOrderStatus: function(status) {
  822. uni.navigateTo({ url: '/pages/shop/order-list/order-list?status=' + status })
  823. },
  824. goToCart: function() {
  825. uni.navigateTo({ url: '/pages/shop/cart/cart' })
  826. },
  827. goToAddress: function() {
  828. uni.navigateTo({ url: '/pages/shop/address/address' })
  829. },
  830. goToSettings: function() {
  831. uni.showActionSheet({
  832. itemList: ['完善信息', '家庭成员', '隐私政策', '用户协议', '我的画像'],
  833. success: function(res) {
  834. var urls = {
  835. 0: '/pages/user-edit/user-edit',
  836. 1: '/pages/profile-extra/family-members',
  837. 2: '/pages/policy/privacy',
  838. 3: '/pages/policy/agreement',
  839. 4: '/pages/profile/portrait-edit'
  840. }
  841. if (urls[res.tapIndex]) {
  842. uni.navigateTo({ url: urls[res.tapIndex] })
  843. }
  844. }
  845. })
  846. },
  847. memberRoleLabel: function(role) {
  848. var map = { parent: '家长', child: '成员', teacher: '规划师' }
  849. return map[role] || '成员'
  850. },
  851. onExitSwitch: function() {
  852. var self = this
  853. uni.showModal({
  854. title: '退出切换',
  855. content: '请输入家长密码以确认退出',
  856. editable: true,
  857. success: function(res) {
  858. if (res.confirm && res.content) {
  859. verifyPassword(res.content).then(function(result) {
  860. if (result.code === 200) {
  861. self.$store.commit('switchBackToParent')
  862. self.isSwitchedChild = false
  863. self.role = 'parent'
  864. uni.showToast({ title: '已退出切换', icon: 'success' })
  865. uni.reLaunch({ url: '/pages/index-home/index' })
  866. } else {
  867. uni.showToast({ title: '密码错误', icon: 'none' })
  868. }
  869. }).catch(function() {
  870. uni.showToast({ title: '验证失败', icon: 'none' })
  871. })
  872. }
  873. }
  874. })
  875. },
  876. logout: function() {
  877. uni.showModal({
  878. title: '确认退出',
  879. content: '确定要退出登录吗?',
  880. success: function(res) {
  881. if (res.confirm) {
  882. this.$store.commit('logout')
  883. uni.reLaunch({ url: '/pages/index-home/index' })
  884. }
  885. }.bind(this)
  886. })
  887. }
  888. }
  889. }
  890. </script>
  891. <style scoped>
  892. .container {
  893. min-height: 100vh;
  894. background: #F8F4EF;
  895. padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
  896. }
  897. /* 登录引导 */
  898. .login-prompt {
  899. display: flex;
  900. flex-direction: column;
  901. align-items: center;
  902. padding: 200rpx 60rpx;
  903. }
  904. .prompt-icon { font-size: 100rpx; margin-bottom: 30rpx; }
  905. .prompt-title { font-size: 40rpx; font-weight: bold; color: #333; margin-bottom: 16rpx; }
  906. .prompt-desc { font-size: 26rpx; color: #999; margin-bottom: 20rpx; }
  907. .login-btn {
  908. background: linear-gradient(135deg, #667eea, #764ba2);
  909. color: #fff;
  910. border-radius: 50rpx;
  911. font-size: 30rpx;
  912. padding: 24rpx 80rpx;
  913. }
  914. .skip-link {
  915. font-size: 26rpx;
  916. color: #999;
  917. margin-top: 24rpx;
  918. text-decoration: underline;
  919. }
  920. .philosophy-card {
  921. margin: 0 40rpx 48rpx;
  922. background: linear-gradient(135deg, #FFF7ED, rgba(249, 115, 22, 0.06));
  923. border-radius: 20rpx;
  924. padding: 28rpx 32rpx;
  925. border-left: 6rpx solid #F97316;
  926. }
  927. .philosophy-title {
  928. font-size: 28rpx;
  929. font-weight: bold;
  930. color: #333;
  931. display: block;
  932. margin-bottom: 8rpx;
  933. }
  934. .philosophy-desc {
  935. font-size: 24rpx;
  936. color: #888;
  937. line-height: 1.6;
  938. display: block;
  939. }
  940. /* 退出切换 */
  941. .exit-switch {
  942. margin: 0 30rpx 16rpx;
  943. background: linear-gradient(135deg, #667eea, #764ba2);
  944. color: #fff;
  945. padding: 16rpx;
  946. border-radius: 12rpx;
  947. text-align: center;
  948. font-size: 26rpx;
  949. }
  950. /* 身份卡 */
  951. .user-card {
  952. margin: 24rpx 30rpx 20rpx;
  953. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  954. border-radius: 24rpx;
  955. padding: 32rpx 32rpx;
  956. display: flex;
  957. justify-content: space-between;
  958. align-items: center;
  959. box-shadow: 0 8rpx 32rpx rgba(102,126,234,0.3);
  960. }
  961. .user-card-left {
  962. display: flex;
  963. align-items: center;
  964. flex: 1;
  965. }
  966. .avatar-wrap {
  967. width: 100rpx;
  968. height: 100rpx;
  969. border-radius: 50%;
  970. background: rgba(255,255,255,0.2);
  971. display: flex;
  972. align-items: center;
  973. justify-content: center;
  974. margin-right: 20rpx;
  975. overflow: hidden;
  976. border: 3rpx solid rgba(255,255,255,0.3);
  977. }
  978. .avatar-img {
  979. width: 100rpx;
  980. height: 100rpx;
  981. border-radius: 50%;
  982. }
  983. .avatar-text {
  984. font-size: 44rpx;
  985. color: #fff;
  986. }
  987. .user-meta { flex: 1; }
  988. .user-name-row {
  989. display: flex;
  990. align-items: center;
  991. gap: 12rpx;
  992. margin-bottom: 8rpx;
  993. }
  994. .nickname {
  995. font-size: 34rpx;
  996. font-weight: bold;
  997. color: #fff;
  998. }
  999. .role-tag {
  1000. font-size: 20rpx;
  1001. background: rgba(255,255,255,0.2);
  1002. color: #fff;
  1003. padding: 4rpx 14rpx;
  1004. border-radius: 20rpx;
  1005. }
  1006. .referrer-line {
  1007. font-size: 24rpx;
  1008. color: rgba(255,255,255,0.7);
  1009. margin-bottom: 6rpx;
  1010. }
  1011. .user-sub {
  1012. display: flex;
  1013. align-items: center;
  1014. gap: 12rpx;
  1015. }
  1016. .level-badge {
  1017. font-size: 22rpx;
  1018. padding: 6rpx 20rpx;
  1019. border-radius: 22rpx;
  1020. font-weight: bold;
  1021. border: 2rpx solid rgba(255,255,255,0.35);
  1022. display: flex;
  1023. align-items: center;
  1024. gap: 6rpx;
  1025. box-sizing: border-box;
  1026. height: 40rpx;
  1027. }
  1028. .level-badge::before {
  1029. content: '👑';
  1030. font-size: 18rpx;
  1031. }
  1032. .level-FREE { background: linear-gradient(135deg, #9CA3AF, #6B7280); color: #fff; }
  1033. .level-FREE::before { content: ''; }
  1034. .level-FAMILY { background: linear-gradient(135deg, #F59E0B, #D97706); color: #fff; }
  1035. .level-PREMIUM { background: linear-gradient(135deg, #6366F1, #4F46E5); color: #fff; }
  1036. .level-LIFETIME {
  1037. background: linear-gradient(135deg, #EC4899, #DB2777);
  1038. color: #fff;
  1039. box-shadow: 0 4rpx 16rpx rgba(236,72,153,0.45);
  1040. border-color: rgba(255,255,255,0.55);
  1041. }
  1042. .days-left {
  1043. font-size: 20rpx;
  1044. color: rgba(255,255,255,0.85);
  1045. }
  1046. .upgrade-btn {
  1047. background: rgba(255,255,255,0.22);
  1048. border: 2rpx solid rgba(255,255,255,0.6);
  1049. border-radius: 20rpx;
  1050. padding: 6rpx 20rpx;
  1051. flex-shrink: 0;
  1052. box-sizing: border-box;
  1053. height: 40rpx;
  1054. display: flex;
  1055. align-items: center;
  1056. }
  1057. .upgrade-btn-text {
  1058. font-size: 20rpx;
  1059. font-weight: bold;
  1060. color: #fff;
  1061. line-height: 1;
  1062. }
  1063. .user-card-right {
  1064. display: flex;
  1065. flex-direction: column;
  1066. align-items: center;
  1067. gap: 6rpx;
  1068. background: rgba(255,255,255,0.15);
  1069. padding: 12rpx 24rpx;
  1070. border-radius: 14rpx;
  1071. }
  1072. .points-num {
  1073. font-size: 36rpx;
  1074. font-weight: bold;
  1075. color: #fff;
  1076. }
  1077. .points-label {
  1078. font-size: 20rpx;
  1079. color: rgba(255,255,255,0.7);
  1080. }
  1081. /* 家庭成员速览 */
  1082. .family-strip {
  1083. margin: 0 30rpx 20rpx;
  1084. background: #fff;
  1085. border-radius: 20rpx;
  1086. padding: 20rpx 24rpx 16rpx;
  1087. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1088. }
  1089. .family-strip-header {
  1090. display: flex;
  1091. justify-content: space-between;
  1092. align-items: center;
  1093. margin-bottom: 14rpx;
  1094. }
  1095. .family-strip-title {
  1096. font-size: 26rpx;
  1097. font-weight: bold;
  1098. color: #333;
  1099. }
  1100. .family-strip-count {
  1101. font-size: 22rpx;
  1102. color: #999;
  1103. }
  1104. .family-avatars {
  1105. display: flex;
  1106. flex-direction: row;
  1107. align-items: center;
  1108. gap: 16rpx;
  1109. overflow-x: auto;
  1110. padding-bottom: 4rpx;
  1111. }
  1112. .family-avatar-item {
  1113. display: flex;
  1114. flex-direction: column;
  1115. align-items: center;
  1116. gap: 6rpx;
  1117. flex-shrink: 0;
  1118. }
  1119. .family-avatar-wrap {
  1120. width: 72rpx;
  1121. height: 72rpx;
  1122. border-radius: 50%;
  1123. background: #f0f0f0;
  1124. display: flex;
  1125. align-items: center;
  1126. justify-content: center;
  1127. overflow: hidden;
  1128. position: relative;
  1129. }
  1130. .family-avatar-img {
  1131. width: 72rpx;
  1132. height: 72rpx;
  1133. border-radius: 50%;
  1134. }
  1135. .family-avatar-text {
  1136. font-size: 28rpx;
  1137. color: #666;
  1138. }
  1139. .family-avatar-wrap.family-avatar-add {
  1140. background: #F8F4EF;
  1141. border: 2rpx dashed #ccc;
  1142. }
  1143. .family-avatar-plus {
  1144. font-size: 36rpx;
  1145. color: #999;
  1146. }
  1147. .family-avatar-tag {
  1148. position: absolute;
  1149. bottom: -4rpx;
  1150. right: -4rpx;
  1151. font-size: 14rpx;
  1152. padding: 2rpx 6rpx;
  1153. border-radius: 8rpx;
  1154. color: #fff;
  1155. }
  1156. .family-avatar-tag.tag-parent { background: #6366F1; }
  1157. .family-avatar-tag.tag-child { background: #F97316; }
  1158. .family-avatar-tag.tag-teacher { background: #10B981; }
  1159. .family-avatar-tag.tag-member { background: #9CA3AF; }
  1160. .family-avatar-name {
  1161. font-size: 20rpx;
  1162. color: #666;
  1163. white-space: nowrap;
  1164. }
  1165. /* 个人成就 */
  1166. .achievement-section {
  1167. margin: 0 30rpx 20rpx;
  1168. background: #fff;
  1169. border-radius: 20rpx;
  1170. padding: 24rpx;
  1171. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1172. }
  1173. .section-header-row {
  1174. display: flex;
  1175. justify-content: space-between;
  1176. align-items: baseline;
  1177. margin-bottom: 16rpx;
  1178. }
  1179. .section-title {
  1180. font-size: 28rpx;
  1181. font-weight: bold;
  1182. color: #333;
  1183. }
  1184. .section-sub {
  1185. font-size: 22rpx;
  1186. color: #999;
  1187. }
  1188. .achievement-grid {
  1189. display: flex;
  1190. flex-wrap: wrap;
  1191. gap: 0;
  1192. }
  1193. .ach-item {
  1194. width: 25%;
  1195. display: flex;
  1196. flex-direction: column;
  1197. align-items: center;
  1198. gap: 6rpx;
  1199. padding: 10rpx 0;
  1200. }
  1201. .ach-icon-wrap {
  1202. width: 64rpx;
  1203. height: 64rpx;
  1204. border-radius: 50%;
  1205. display: flex;
  1206. align-items: center;
  1207. justify-content: center;
  1208. margin-bottom: 4rpx;
  1209. }
  1210. .ach-icon {
  1211. font-size: 32rpx;
  1212. line-height: 1;
  1213. }
  1214. .ach-num {
  1215. font-size: 28rpx;
  1216. font-weight: bold;
  1217. color: #333;
  1218. line-height: 1;
  1219. }
  1220. .ach-label {
  1221. font-size: 20rpx;
  1222. color: #999;
  1223. line-height: 1;
  1224. }
  1225. /* 我的订单(电商风格) */
  1226. .order-section {
  1227. margin: 0 30rpx 20rpx;
  1228. }
  1229. .order-section-header {
  1230. display: flex;
  1231. align-items: center;
  1232. justify-content: space-between;
  1233. margin-bottom: 14rpx;
  1234. }
  1235. .order-all-link {
  1236. font-size: 24rpx;
  1237. color: #999;
  1238. }
  1239. .order-card {
  1240. background: #fff;
  1241. border-radius: 20rpx;
  1242. padding: 24rpx 12rpx;
  1243. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
  1244. }
  1245. .order-grid {
  1246. display: flex;
  1247. flex-wrap: wrap;
  1248. }
  1249. .order-grid-item {
  1250. width: 20%;
  1251. display: flex;
  1252. flex-direction: column;
  1253. align-items: center;
  1254. gap: 10rpx;
  1255. padding: 8rpx 0;
  1256. }
  1257. .order-icon-wrap {
  1258. position: relative;
  1259. width: 64rpx;
  1260. height: 64rpx;
  1261. display: flex;
  1262. align-items: center;
  1263. justify-content: center;
  1264. }
  1265. .order-icon {
  1266. font-size: 40rpx;
  1267. line-height: 1;
  1268. }
  1269. .order-badge {
  1270. position: absolute;
  1271. top: -8rpx;
  1272. right: -14rpx;
  1273. min-width: 30rpx;
  1274. height: 30rpx;
  1275. line-height: 30rpx;
  1276. border-radius: 15rpx;
  1277. background: #FF4D4F;
  1278. color: #fff;
  1279. font-size: 18rpx;
  1280. text-align: center;
  1281. padding: 0 6rpx;
  1282. }
  1283. .order-grid-text {
  1284. font-size: 22rpx;
  1285. color: #666;
  1286. line-height: 1;
  1287. }
  1288. .order-extra-row {
  1289. display: flex;
  1290. align-items: center;
  1291. margin-top: 20rpx;
  1292. padding-top: 20rpx;
  1293. border-top: 1rpx solid #F2F2F2;
  1294. }
  1295. .order-extra-item {
  1296. flex: 1;
  1297. display: flex;
  1298. align-items: center;
  1299. justify-content: center;
  1300. gap: 8rpx;
  1301. }
  1302. .order-extra-icon {
  1303. font-size: 28rpx;
  1304. }
  1305. .order-extra-text {
  1306. font-size: 24rpx;
  1307. color: #333;
  1308. }
  1309. .order-extra-badge {
  1310. min-width: 30rpx;
  1311. height: 30rpx;
  1312. line-height: 30rpx;
  1313. border-radius: 15rpx;
  1314. background: #F97316;
  1315. color: #fff;
  1316. font-size: 18rpx;
  1317. text-align: center;
  1318. padding: 0 6rpx;
  1319. }
  1320. .order-extra-divider {
  1321. width: 1rpx;
  1322. height: 30rpx;
  1323. background: #F2F2F2;
  1324. }
  1325. /* 规划师服务中心入口 */
  1326. .service-center-section {
  1327. margin: 0 30rpx 20rpx;
  1328. }
  1329. .service-center-card {
  1330. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1331. border-radius: 20rpx;
  1332. padding: 24rpx;
  1333. display: flex;
  1334. align-items: center;
  1335. gap: 20rpx;
  1336. box-shadow: 0 4rpx 16rpx rgba(102,126,234,0.25);
  1337. }
  1338. .service-center-card.disabled-card {
  1339. background: linear-gradient(135deg, #e2e8f0, #94a3b8);
  1340. box-shadow: none;
  1341. opacity: 0.8;
  1342. }
  1343. .service-center-icon {
  1344. font-size: 48rpx;
  1345. }
  1346. .service-center-info {
  1347. flex: 1;
  1348. }
  1349. .service-center-title {
  1350. font-size: 30rpx;
  1351. font-weight: bold;
  1352. color: #fff;
  1353. display: block;
  1354. }
  1355. .service-center-desc {
  1356. font-size: 22rpx;
  1357. color: rgba(255,255,255,0.8);
  1358. margin-top: 6rpx;
  1359. display: block;
  1360. }
  1361. .service-center-card.disabled-card .service-center-title {
  1362. color: #fff;
  1363. }
  1364. .service-center-card.disabled-card .service-center-desc {
  1365. color: rgba(255,255,255,0.9);
  1366. }
  1367. .service-center-arrow {
  1368. font-size: 40rpx;
  1369. color: #fff;
  1370. }
  1371. /* 设置 + 退出 */
  1372. .bottom-menu-section {
  1373. margin: 0 30rpx;
  1374. padding-bottom: 20rpx;
  1375. }
  1376. .menu-row {
  1377. width: 100%;
  1378. background: #fff;
  1379. border-radius: 16rpx;
  1380. padding: 24rpx;
  1381. display: flex;
  1382. align-items: center;
  1383. gap: 14rpx;
  1384. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  1385. margin-bottom: 20rpx;
  1386. }
  1387. .menu-row-icon {
  1388. font-size: 30rpx;
  1389. }
  1390. .menu-row-text {
  1391. flex: 1;
  1392. font-size: 26rpx;
  1393. color: #333;
  1394. }
  1395. .menu-row-arrow {
  1396. font-size: 28rpx;
  1397. color: #ccc;
  1398. }
  1399. .logout-btn {
  1400. width: 100%;
  1401. background: #fff;
  1402. border-radius: 16rpx;
  1403. padding: 24rpx;
  1404. text-align: center;
  1405. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  1406. }
  1407. .logout-text {
  1408. font-size: 26rpx;
  1409. color: #FF4D4F;
  1410. font-weight: 500;
  1411. }
  1412. /* 分享推广码浮动按钮 */
  1413. .share-qr-float {
  1414. position: fixed;
  1415. top: 180rpx;
  1416. right: 30rpx;
  1417. width: 80rpx;
  1418. height: 80rpx;
  1419. border-radius: 50%;
  1420. background: #fff;
  1421. box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.15);
  1422. display: flex;
  1423. align-items: center;
  1424. justify-content: center;
  1425. z-index: 99;
  1426. }
  1427. /* 二维码图标(纯 CSS 绘制,不依赖 emoji 字体) */
  1428. .qr-glyph {
  1429. position: relative;
  1430. width: 46rpx;
  1431. height: 46rpx;
  1432. }
  1433. .qr-eye {
  1434. position: absolute;
  1435. width: 16rpx;
  1436. height: 16rpx;
  1437. border: 4rpx solid #1E293B;
  1438. border-radius: 3rpx;
  1439. box-sizing: border-box;
  1440. }
  1441. .qr-eye-tl { left: 0; top: 0; }
  1442. .qr-eye-tr { right: 0; top: 0; }
  1443. .qr-eye-bl { left: 0; bottom: 0; }
  1444. .qr-mid {
  1445. position: absolute;
  1446. left: 20rpx;
  1447. top: 20rpx;
  1448. width: 7rpx;
  1449. height: 7rpx;
  1450. background: #1E293B;
  1451. border-radius: 1rpx;
  1452. }
  1453. .qr-cluster {
  1454. position: absolute;
  1455. right: 0;
  1456. bottom: 0;
  1457. width: 18rpx;
  1458. display: flex;
  1459. flex-wrap: wrap;
  1460. }
  1461. .qr-pip {
  1462. width: 7rpx;
  1463. height: 7rpx;
  1464. margin: 1rpx;
  1465. background: #1E293B;
  1466. border-radius: 1rpx;
  1467. }
  1468. </style>