選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 

493 行
12 KiB

  1. <template>
  2. <view class="profile">
  3. <ai-nav-title title="个人中心" />
  4. <scroll-view class="page-scroll" scroll-y>
  5. <view class="profile-content">
  6. <view class="balance-card" :class="{ 'guest-card': !userStore.isLogin }">
  7. <image class="balance-texture" src="https://cdn.aionline.cc/ai/profile/balance_texture.png" mode="aspectFit" />
  8. <view v-if="userStore.isLogin" class="balance-copy">
  9. <text class="hello">{{ `Hi,${userStore.nickname || 'AI在线用户'}!` }}</text>
  10. <text class="balance-label">点数余额</text>
  11. <view class="balance-bottom">
  12. <text class="balance-value">{{ userStore.pointsText }}</text>
  13. <view class="balance-actions">
  14. <view class="outline-btn" @click="go('bill')">账单</view>
  15. <view class="solid-btn" @click="go('recharge')">充值</view>
  16. </view>
  17. </view>
  18. </view>
  19. <view v-else class="guest-copy">
  20. <view class="guest-main">
  21. <view class="guest-avatar">
  22. <up-icon name="account-fill" color="#2384f8" size="28" />
  23. </view>
  24. <view class="guest-text">
  25. <text class="guest-title">登录 AI在线</text>
  26. <text class="guest-description">同步点数、作品与创作记录</text>
  27. </view>
  28. </view>
  29. <view class="guest-bottom">
  30. <text class="guest-tip">登录后可跨设备查看创作数据</text>
  31. <view class="login-btn" hover-class="login-btn--pressed" @click="requireLogin">
  32. <text>立即登录</text>
  33. <up-icon name="arrow-right" color="#2384f8" size="14" />
  34. </view>
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 按用途分组,组间留白,避免一长条列表 -->
  39. <view v-for="group in menuGroups" :key="group.key" class="menu-group">
  40. <text class="group-title">{{ group.title }}</text>
  41. <view class="menu-panel">
  42. <view v-for="item in group.items" :key="item.key" class="menu-row" @click="onMenu(item)">
  43. <view class="menu-icon" :style="{ background: item.iconBg }">
  44. <up-icon :name="item.icon" :color="item.iconColor" size="20" />
  45. </view>
  46. <text class="menu-name">{{ item.name }}</text>
  47. <text v-if="item.hint" class="menu-hint">{{ item.hint }}</text>
  48. <up-icon name="arrow-right" color="#c3cddb" size="14" />
  49. </view>
  50. </view>
  51. </view>
  52. </view>
  53. </scroll-view>
  54. </view>
  55. </template>
  56. <script setup>
  57. import { computed, ref } from 'vue'
  58. import { onShow } from '@dcloudio/uni-app'
  59. import { inviteApi, pointApi } from '@/api/index.js'
  60. import { useUserStore } from '@/store/user.js'
  61. import { useApp } from '@/utils/useApp.js'
  62. const userStore = useUserStore()
  63. const { toast } = useApp()
  64. // 图标统一用 uview-plus 的图标字体,不再各自维护 png
  65. const earnMenuConfig = {
  66. key: 'earn',
  67. name: '获得点数',
  68. hint: '完成任务获得点数',
  69. icon: 'gift',
  70. iconColor: '#ff8a00',
  71. iconBg: '#fff5e8',
  72. auth: false
  73. }
  74. const inviteMenuConfig = {
  75. key: 'invite',
  76. name: '邀请好友',
  77. hint: '得100点分享奖励',
  78. icon: 'man-add',
  79. iconColor: '#17a65a',
  80. iconBg: '#eefaf3',
  81. auth: true
  82. }
  83. // 福利组:签到、邀请与看广告都受后台开关控制,关掉就整项移除
  84. const benefitMenus = ref([{ ...inviteMenuConfig }])
  85. const serviceMenus = [
  86. { key: 'official', name: '关注公众号', icon: 'weixin-fill', iconColor: '#07c160', iconBg: '#eafaf0' },
  87. { key: 'contact', name: '联系我们', icon: 'kefu-ermai', iconColor: '#1f8cff', iconBg: '#eef7ff' },
  88. { key: 'feedback', name: '意见反馈', icon: 'edit-pen', iconColor: '#7a5af8', iconBg: '#f3f0ff', auth: true }
  89. ]
  90. // 服务条款、隐私政策、当前版本、退出登录都收进设置二级页
  91. const otherMenus = [
  92. { key: 'setting', name: '设置', icon: 'setting', iconColor: '#64748b', iconBg: '#f1f5fa' }
  93. ]
  94. const menuGroups = computed(() => {
  95. const groups = []
  96. if (benefitMenus.value.length) {
  97. groups.push({ key: 'benefit', title: '福利', items: benefitMenus.value })
  98. }
  99. groups.push({ key: 'service', title: '服务与帮助', items: serviceMenus })
  100. groups.push({ key: 'other', title: '其他', items: otherMenus })
  101. return groups
  102. })
  103. const MENU_ROUTE = {
  104. earn: '/pages/point/earn',
  105. invite: '/pages/invite/invite',
  106. official: '/pages/my/wechat',
  107. feedback: '/pages/my/feedback',
  108. contact: '/pages/my/contact',
  109. setting: '/pages/my/setting'
  110. }
  111. function requireLogin() {
  112. if (userStore.isLogin) return true
  113. uni.navigateTo({ url: '/pages/login/login' })
  114. return false
  115. }
  116. function onMenu(item) {
  117. if (item.auth && !requireLogin()) return
  118. if (item.soon) return toast(`${item.name} 即将上线`)
  119. const route = MENU_ROUTE[item.key]
  120. if (route) return uni.navigateTo({ url: route })
  121. toast(`${item.name}(待开发)`)
  122. }
  123. function go(page) {
  124. if (!requireLogin()) return
  125. if (page === 'bill') return uni.navigateTo({ url: '/pages/bill/bill?type=recharge' })
  126. if (page === 'recharge') return uni.navigateTo({ url: '/pages/recharge/recharge' })
  127. toast(`${page}(待开发)`)
  128. }
  129. function syncInviteMenu(config = {}) {
  130. const currentIndex = benefitMenus.value.findIndex((item) => item.key === 'invite')
  131. if (Number(config.status) !== 1) {
  132. if (currentIndex > -1) benefitMenus.value.splice(currentIndex, 1)
  133. return
  134. }
  135. const points = Number(config.inviter_points || 0)
  136. const item = {
  137. ...inviteMenuConfig,
  138. hint: points > 0 ? `得${points}点分享奖励` : '分享给好友'
  139. }
  140. if (currentIndex > -1) {
  141. benefitMenus.value.splice(currentIndex, 1, item)
  142. return
  143. }
  144. // 排在「获得点数」之后,没有该入口时排在最前
  145. const earnIndex = benefitMenus.value.findIndex((menu) => menu.key === 'earn')
  146. benefitMenus.value.splice(earnIndex > -1 ? earnIndex + 1 : 0, 0, item)
  147. }
  148. async function loadProfile() {
  149. if (!userStore.isLogin) return
  150. try {
  151. await Promise.all([userStore.fetchUserInfo(), userStore.fetchBalance()])
  152. } catch (e) {
  153. // 登录失效由 request 统一处理
  154. }
  155. }
  156. async function loadInviteConfig() {
  157. try {
  158. const res = await inviteApi.getConfig()
  159. const config = (res.data && res.data.config) || {}
  160. syncInviteMenu(config)
  161. } catch (e) {}
  162. }
  163. /** 所有免费任务都关闭时不显示入口,而不是显示后再提示不可用 */
  164. function syncEarnMenu(enabled, hint) {
  165. const currentIndex = benefitMenus.value.findIndex((item) => item.key === 'earn')
  166. if (!enabled) {
  167. if (currentIndex > -1) benefitMenus.value.splice(currentIndex, 1)
  168. return
  169. }
  170. const item = { ...earnMenuConfig, hint: hint || earnMenuConfig.hint }
  171. if (currentIndex > -1) {
  172. benefitMenus.value.splice(currentIndex, 1, item)
  173. return
  174. }
  175. // 获得点数放在福利组最前面
  176. benefitMenus.value.splice(0, 0, item)
  177. }
  178. async function loadEarnConfig() {
  179. try {
  180. const res = await pointApi.getEarnConfig()
  181. const data = res.data || {}
  182. const checkinPoints = Number((data.checkin && data.checkin.points_per_checkin) || 0)
  183. const adPoints = Number((data.ad && data.ad.points_per_ad) || 0)
  184. let hint = '完成任务获得点数'
  185. if (data.checkin && data.checkin.enabled && checkinPoints > 0) hint = `每日签到获得${checkinPoints}点`
  186. else if (data.ad && data.ad.enabled && adPoints > 0) hint = `看视频获得${adPoints}点`
  187. syncEarnMenu(Boolean(data.entry_enabled), hint)
  188. } catch (e) {}
  189. }
  190. onShow(() => {
  191. loadInviteConfig()
  192. loadEarnConfig()
  193. loadProfile()
  194. })
  195. </script>
  196. <style lang="scss" scoped>
  197. .profile {
  198. height: 100vh;
  199. display: flex;
  200. flex-direction: column;
  201. overflow: hidden;
  202. background: #f3f3f3;
  203. }
  204. .page-scroll {
  205. flex: 1;
  206. height: 0;
  207. }
  208. .profile-content {
  209. min-height: 100vh;
  210. padding: 30rpx 30rpx 120rpx;
  211. box-sizing: border-box;
  212. background: #f3f3f3;
  213. }
  214. .balance-card {
  215. position: relative;
  216. height: 266rpx;
  217. border-radius: 24rpx;
  218. overflow: hidden;
  219. background: linear-gradient(135deg, #3f94ff 0%, #2384f8 100%);
  220. box-sizing: border-box;
  221. }
  222. .guest-card {
  223. background: linear-gradient(135deg, #4298ff 0%, #1677ef 100%);
  224. }
  225. .balance-texture {
  226. position: absolute;
  227. right: 13rpx;
  228. top: -44rpx;
  229. width: 269rpx;
  230. height: 288rpx;
  231. }
  232. .balance-copy {
  233. position: relative;
  234. z-index: 1;
  235. height: 100%;
  236. padding: 44rpx 62rpx;
  237. display: flex;
  238. flex-direction: column;
  239. box-sizing: border-box;
  240. }
  241. .guest-copy {
  242. position: relative;
  243. z-index: 1;
  244. height: 100%;
  245. padding: 36rpx 42rpx 34rpx;
  246. display: flex;
  247. flex-direction: column;
  248. box-sizing: border-box;
  249. }
  250. .guest-main {
  251. display: flex;
  252. align-items: center;
  253. min-width: 0;
  254. }
  255. .guest-avatar {
  256. width: 84rpx;
  257. height: 84rpx;
  258. border-radius: 50%;
  259. display: flex;
  260. align-items: center;
  261. justify-content: center;
  262. flex-shrink: 0;
  263. background: rgba(255, 255, 255, 0.96);
  264. box-shadow: 0 10rpx 24rpx rgba(16, 87, 174, 0.2);
  265. }
  266. .guest-text {
  267. margin-left: 24rpx;
  268. min-width: 0;
  269. display: flex;
  270. flex-direction: column;
  271. }
  272. .guest-title {
  273. color: #ffffff;
  274. font-size: 34rpx;
  275. font-weight: 700;
  276. line-height: 1.25;
  277. }
  278. .guest-description {
  279. margin-top: 8rpx;
  280. color: rgba(255, 255, 255, 0.78);
  281. font-size: 24rpx;
  282. line-height: 1.4;
  283. }
  284. .guest-bottom {
  285. margin-top: auto;
  286. display: flex;
  287. align-items: center;
  288. justify-content: space-between;
  289. gap: 20rpx;
  290. }
  291. .guest-tip {
  292. color: rgba(255, 255, 255, 0.68);
  293. font-size: 22rpx;
  294. line-height: 1.35;
  295. }
  296. .login-btn {
  297. min-width: 180rpx;
  298. height: 64rpx;
  299. padding: 0 24rpx 0 28rpx;
  300. border-radius: 32rpx;
  301. display: flex;
  302. align-items: center;
  303. justify-content: center;
  304. gap: 6rpx;
  305. flex-shrink: 0;
  306. color: #2384f8;
  307. font-size: 27rpx;
  308. font-weight: 700;
  309. background: #ffffff;
  310. box-shadow: 0 10rpx 24rpx rgba(16, 87, 174, 0.18);
  311. box-sizing: border-box;
  312. }
  313. .login-btn--pressed {
  314. opacity: 0.84;
  315. transform: scale(0.98);
  316. }
  317. .hello {
  318. max-width: 430rpx;
  319. color: #ffffff;
  320. font-size: 27rpx;
  321. line-height: 1;
  322. overflow: hidden;
  323. text-overflow: ellipsis;
  324. white-space: nowrap;
  325. }
  326. .balance-label {
  327. margin-top: auto;
  328. color: rgba(255, 255, 255, 0.65);
  329. font-size: 25rpx;
  330. line-height: 1;
  331. }
  332. .balance-value {
  333. color: #ffffff;
  334. font-size: 40rpx;
  335. font-weight: 700;
  336. line-height: 1;
  337. }
  338. .balance-bottom {
  339. margin-top: 16rpx;
  340. display: flex;
  341. align-items: center;
  342. justify-content: space-between;
  343. gap: 24rpx;
  344. }
  345. .balance-actions {
  346. display: flex;
  347. gap: 22rpx;
  348. flex-shrink: 0;
  349. }
  350. .outline-btn,
  351. .solid-btn {
  352. width: 127rpx;
  353. height: 58rpx;
  354. border-radius: 29rpx;
  355. display: flex;
  356. align-items: center;
  357. justify-content: center;
  358. font-size: 27rpx;
  359. box-sizing: border-box;
  360. }
  361. .outline-btn {
  362. color: #ffffff;
  363. border: 1rpx solid rgba(255, 255, 255, 0.75);
  364. background: rgba(255, 255, 255, 0.08);
  365. }
  366. .solid-btn {
  367. color: #ffffff;
  368. border: 1rpx solid rgba(255, 255, 255, 0.75);
  369. background: rgba(255, 255, 255, 0.08);
  370. }
  371. .menu-group {
  372. margin-top: 26rpx;
  373. }
  374. .group-title {
  375. display: block;
  376. margin: 0 8rpx 12rpx;
  377. color: #8a95a6;
  378. font-size: 23rpx;
  379. font-weight: 800;
  380. }
  381. .menu-panel {
  382. border-radius: 21rpx;
  383. background: #ffffff;
  384. overflow: hidden;
  385. box-sizing: border-box;
  386. }
  387. .menu-row {
  388. position: relative;
  389. min-height: 108rpx;
  390. padding: 20rpx 27rpx;
  391. display: flex;
  392. align-items: center;
  393. box-sizing: border-box;
  394. /* 分隔线只在图标右侧起始,视觉上更整齐 */
  395. &::after {
  396. content: '';
  397. position: absolute;
  398. left: 105rpx;
  399. right: 24rpx;
  400. bottom: 0;
  401. height: 1rpx;
  402. background: #f1f5fa;
  403. }
  404. &:last-child::after {
  405. display: none;
  406. }
  407. }
  408. .menu-icon {
  409. width: 68rpx;
  410. height: 68rpx;
  411. border-radius: 18rpx;
  412. display: flex;
  413. align-items: center;
  414. justify-content: center;
  415. flex-shrink: 0;
  416. }
  417. .menu-name {
  418. margin-left: 22rpx;
  419. color: #172033;
  420. font-size: 29rpx;
  421. line-height: 1.3;
  422. flex: 1;
  423. min-width: 0;
  424. }
  425. .menu-hint {
  426. margin-left: 20rpx;
  427. max-width: 280rpx;
  428. color: #8a95a6;
  429. font-size: 24rpx;
  430. line-height: 1.3;
  431. overflow: hidden;
  432. text-overflow: ellipsis;
  433. white-space: nowrap;
  434. }
  435. </style>