index.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <template>
  2. <view class="page-profile">
  3. <!-- US-5.1 §13: Profile incomplete banner -->
  4. <view v-if="userStore.isLoggedIn && userStore.profileIncomplete" class="banner" @click="goEditProfile">
  5. <text class="banner-text">📝 请完善个人资料,开启能量匹配</text>
  6. <text class="banner-arrow">›</text>
  7. </view>
  8. <!-- US-4.3: Renewal reminder -->
  9. <view v-if="isExpiringSoon" class="renewal-banner" @click="goPayment">
  10. <text class="renewal-text">⏰ 您的会员即将到期,请及时续费</text>
  11. <text class="renewal-arrow">›</text>
  12. </view>
  13. <!-- User info card -->
  14. <view class="user-card" @click="goLogin">
  15. <view class="avatar">
  16. <image :src="userStore.avatarUrl || '/static/default-avatar.png'" mode="aspectFill" />
  17. </view>
  18. <text class="nickname">{{ userStore.nickname || '未登录' }}</text>
  19. <text v-if="!userStore.isLoggedIn" class="login-hint">点击登录</text>
  20. <view v-if="userStore.isVip" class="vip-badge">VIP 会员</view>
  21. </view>
  22. <!-- VIP status -->
  23. <view v-if="userStore.vipType === 'practitioner' && userStore.isVip" class="vip-card pro" @click="goPayment">
  24. <view class="vip-info">
  25. <text class="vip-title">能量师会员</text>
  26. <text class="vip-desc">
  27. 有效期至: {{ userStore.vipEndTime?.slice(0, 10) }}
  28. </text>
  29. </view>
  30. <text class="vip-action">续费</text>
  31. </view>
  32. <view v-else-if="userStore.vipType === 'annual' && userStore.isVip" class="vip-card annual" @click="goUpgradePractitioner">
  33. <view class="vip-info">
  34. <text class="vip-title">C端会员</text>
  35. <text class="vip-desc">
  36. 有效期至: {{ userStore.vipEndTime?.slice(0, 10) }}
  37. </text>
  38. </view>
  39. <text class="vip-action">升级能量师</text>
  40. </view>
  41. <view v-else class="vip-card" @click="goPayment">
  42. <view class="vip-info">
  43. <text class="vip-title">VIP 会员</text>
  44. <text class="vip-desc">
  45. 开通VIP获取完整分析
  46. </text>
  47. </view>
  48. <text class="vip-action">立即开通</text>
  49. </view>
  50. <!-- Daily quota (non-VIP) -->
  51. <view v-if="!userStore.isVip" class="quota-card">
  52. <view class="quota-item">
  53. <text class="quota-label">今日分析</text>
  54. <text class="quota-value">{{ userStore.usedCharts }}/{{ userStore.maxCharts }}</text>
  55. </view>
  56. <view class="quota-divider"></view>
  57. <view class="quota-item">
  58. <text class="quota-label">今日解读</text>
  59. <text class="quota-value">{{ userStore.usedChats }}/{{ userStore.maxChats }}</text>
  60. </view>
  61. </view>
  62. <!-- Menu list -->
  63. <view class="menu-section">
  64. <view class="menu-item" @click="goEditProfile">
  65. <text>✏️ 编辑资料</text>
  66. <text class="menu-arrow">›</text>
  67. </view>
  68. <view class="menu-item" @click="goRecords">
  69. <text>📋 我的分析</text>
  70. <text class="menu-arrow">›</text>
  71. </view>
  72. <view class="menu-item" @click="goCommission">
  73. <text>💰 我的推广</text>
  74. <text class="menu-arrow">›</text>
  75. </view>
  76. <view class="menu-item" @click="copyReferral">
  77. <text>🔗 推广码: {{ userStore.referralCode || '加载中' }}</text>
  78. <text class="menu-tip">点击复制</text>
  79. </view>
  80. </view>
  81. <!-- Actions -->
  82. <view class="menu-section">
  83. <view class="menu-item" @click="handleLogout">
  84. <text style="color: #EF4444;">退出登录</text>
  85. </view>
  86. </view>
  87. </view>
  88. </template>
  89. <script setup>
  90. import { computed, onShow } from '@dcloudio/uni-app'
  91. import { useUserStore } from '@/stores/user'
  92. const userStore = useUserStore()
  93. // US-4.3: Renewal reminder if expiry within 7 days
  94. const isExpiringSoon = computed(() => {
  95. if (!userStore.vipEndTime) return false
  96. const expiry = new Date(userStore.vipEndTime)
  97. const now = new Date()
  98. const diffHours = (expiry - now) / (1000 * 60 * 60)
  99. return diffHours > 0 && diffHours <= 7 * 24
  100. })
  101. onShow(() => {
  102. if (userStore.isLoggedIn) {
  103. userStore.fetchProfile()
  104. userStore.fetchQuota()
  105. userStore.fetchCommissionSummary()
  106. }
  107. })
  108. function goEditProfile() {
  109. if (!userStore.isLoggedIn) {
  110. uni.navigateTo({ url: '/pages/login/index' })
  111. return
  112. }
  113. uni.navigateTo({ url: '/pages/profile/edit' })
  114. }
  115. function goLogin() {
  116. if (!userStore.isLoggedIn) {
  117. uni.navigateTo({ url: '/pages/login/index' })
  118. }
  119. }
  120. function goPayment() {
  121. uni.navigateTo({ url: '/pages/payment/index?product=annual' })
  122. }
  123. function goUpgradePractitioner() {
  124. uni.navigateTo({ url: '/pages/payment/index?product=practitioner' })
  125. }
  126. function goRecords() {
  127. uni.switchTab({ url: '/pages/records/index' })
  128. }
  129. function goCommission() {
  130. if (!userStore.isLoggedIn) {
  131. uni.navigateTo({ url: '/pages/login/index' })
  132. return
  133. }
  134. uni.navigateTo({ url: '/pages/commission/index' })
  135. }
  136. function copyReferral() {
  137. if (userStore.referralCode) {
  138. uni.setClipboardData({
  139. data: userStore.referralCode,
  140. success: () => uni.showToast({ title: '已复制推广码', icon: 'success' })
  141. })
  142. }
  143. }
  144. function handleLogout() {
  145. uni.showModal({
  146. title: '确认退出',
  147. content: '确定要退出登录吗?',
  148. success: (res) => {
  149. if (res.confirm) {
  150. userStore.logout()
  151. uni.showToast({ title: '已退出', icon: 'success' })
  152. }
  153. }
  154. })
  155. }
  156. </script>
  157. <style scoped lang="scss">
  158. .page-profile { padding: 16px; }
  159. .user-card {
  160. text-align: center; padding: 30px 0; background: #fff;
  161. border-radius: 12px; margin-bottom: 16px;
  162. }
  163. .avatar {
  164. width: 64px; height: 64px; border-radius: 50%; overflow: hidden;
  165. margin: 0 auto 12px;
  166. }
  167. .avatar image { width: 100%; height: 100%; }
  168. .nickname { font-size: 18px; font-weight: 600; display: block; color: #1F2937; }
  169. .login-hint { font-size: 12px; color: #3B82F6; display: block; margin-top: 4px; }
  170. .vip-badge {
  171. display: inline-block; background: #FEF3C7; color: #D97706;
  172. padding: 2px 12px; border-radius: 12px; font-size: 12px; margin-top: 8px;
  173. }
  174. .vip-card {
  175. background: linear-gradient(135deg, #3B82F6, #2563EB); border-radius: 12px;
  176. padding: 16px; display: flex; justify-content: space-between;
  177. align-items: center; margin-bottom: 16px;
  178. }
  179. .vip-card.annual {
  180. background: linear-gradient(135deg, #f59e0b, #d97706);
  181. }
  182. .vip-card.pro {
  183. background: linear-gradient(135deg, #7c3aed, #6d28d9);
  184. }
  185. .vip-title { color: #fff; font-weight: 600; font-size: 16px; display: block; }
  186. .vip-desc { color: rgba(255,255,255,0.8); font-size: 12px; display: block; margin-top: 4px; }
  187. .vip-action { color: #fff; font-size: 14px; font-weight: 500; }
  188. .menu-section { background: #fff; border-radius: 12px; margin-bottom: 16px; overflow: hidden; }
  189. .menu-item {
  190. display: flex; justify-content: space-between; align-items: center;
  191. padding: 14px 16px; border-bottom: 1px solid #F3F4F6;
  192. }
  193. .menu-item:last-child { border-bottom: none; }
  194. .menu-arrow { color: #9CA3AF; font-size: 18px; }
  195. .menu-tip { color: #3B82F6; font-size: 12px; }
  196. .quota-card {
  197. background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px;
  198. display: flex; align-items: center; justify-content: space-around;
  199. }
  200. .quota-item { text-align: center; }
  201. .quota-label { font-size: 12px; color: #6B7280; display: block; margin-bottom: 4px; }
  202. .quota-value { font-size: 20px; font-weight: 700; color: #1F2937; }
  203. .quota-divider { width: 1px; height: 36px; background: #E5E7EB; }
  204. /* US-5.1 §13 profile incomplete banner */
  205. .banner {
  206. background: #FEF3C7; border: 1px solid #FDE68A; border-radius: 10px;
  207. padding: 10px 14px; margin-bottom: 12px;
  208. display: flex; justify-content: space-between; align-items: center;
  209. }
  210. .banner-text { font-size: 13px; color: #92400E; flex: 1; }
  211. .banner-arrow { font-size: 18px; color: #92400E; margin-left: 8px; }
  212. /* US-4.3 Renewal reminder */
  213. .renewal-banner {
  214. background: #FEF3C7;
  215. border: 1px solid #FDE68A;
  216. border-radius: 10px;
  217. padding: 10px 14px;
  218. margin-bottom: 12px;
  219. display: flex;
  220. justify-content: space-between;
  221. align-items: center;
  222. }
  223. .renewal-text {
  224. font-size: 13px;
  225. color: #92400E;
  226. flex: 1;
  227. }
  228. .renewal-arrow {
  229. font-size: 18px;
  230. color: #92400E;
  231. margin-left: 8px;
  232. }
  233. </style>