Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 

466 rindas
10 KiB

  1. <template>
  2. <view class="invite-list-page">
  3. <view class="summary-card">
  4. <view>
  5. <text class="eyebrow">INVITE RECORDS</text>
  6. <text class="summary-title">我的邀请</text>
  7. <text class="summary-desc">好友完成注册后,奖励点数会自动入账。</text>
  8. </view>
  9. <view class="summary-count">
  10. <text class="count-num">{{ total || list.length }}</text>
  11. <text class="count-label">记录</text>
  12. </view>
  13. </view>
  14. <view class="stats-row">
  15. <view class="stat-card">
  16. <text class="stat-num">{{ successCount }}</text>
  17. <text class="stat-label">已奖励</text>
  18. </view>
  19. <view class="stat-card">
  20. <text class="stat-num">{{ rewardPoints }}</text>
  21. <text class="stat-label">累计点数</text>
  22. </view>
  23. <view class="stat-card">
  24. <text class="stat-num">{{ pendingCount }}</text>
  25. <text class="stat-label">待处理</text>
  26. </view>
  27. </view>
  28. <view v-if="list.length" class="record-list">
  29. <view v-for="item in list" :key="item.id" class="record-card">
  30. <view class="avatar-wrap">
  31. <image v-if="item.avatar" class="avatar" :src="item.avatar" mode="aspectFill" />
  32. <text v-else class="avatar-text">{{ getInitial(item.user_name) }}</text>
  33. </view>
  34. <view class="record-main">
  35. <view class="record-head">
  36. <text class="name">{{ item.user_name || '好友' }}</text>
  37. <text class="status" :class="statusClass(item.status)">{{ item.status_text || statusText(item.status) }}</text>
  38. </view>
  39. <view class="reward-line">
  40. <text v-if="Number(item.inviter_points) > 0" class="reward-chip">+{{ item.inviter_points }} 点</text>
  41. <text class="reward-desc">{{ getRecordDesc(item) }}</text>
  42. </view>
  43. <text class="time">{{ formatTime(item.reward_time || item.add_time) }}</text>
  44. </view>
  45. </view>
  46. </view>
  47. <view v-if="!list.length && !loading" class="empty-state">
  48. <view class="empty-mark">邀</view>
  49. <text class="empty-title">还没有邀请记录</text>
  50. <text class="empty-desc">分享给好友,好友注册成功后这里会展示奖励明细。</text>
  51. <view class="empty-action" @click="goInvite">去邀请好友</view>
  52. </view>
  53. <view v-if="loading" class="loading">{{ list.length ? '加载更多...' : '加载中...' }}</view>
  54. <view v-if="list.length && list.length >= total && !loading" class="end-text">没有更多记录了</view>
  55. </view>
  56. </template>
  57. <script setup>
  58. import { computed, ref } from 'vue'
  59. import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
  60. import dayjs from 'dayjs'
  61. import { inviteApi } from '@/api/index.js'
  62. import { useUserStore } from '@/store/user.js'
  63. import { useApp } from '@/utils/useApp.js'
  64. const userStore = useUserStore()
  65. const { toast } = useApp()
  66. const list = ref([])
  67. const stats = ref({})
  68. const page = ref(1)
  69. const pageSize = 10
  70. const total = ref(0)
  71. const loading = ref(false)
  72. const successCount = computed(() => Number(stats.value.success !== undefined ? stats.value.success : list.value.filter((item) => Number(item.status) === 1).length))
  73. const pendingCount = computed(() => list.value.filter((item) => Number(item.status) === 0).length)
  74. const rewardPoints = computed(() => {
  75. if (stats.value.inviter_points !== undefined) return Number(stats.value.inviter_points || 0)
  76. return list.value.reduce((sum, item) => {
  77. if (Number(item.status) !== 1) return sum
  78. return sum + Number(item.inviter_points || 0)
  79. }, 0)
  80. })
  81. onLoad(() => {
  82. userStore.restore()
  83. if (!userStore.isLogin) {
  84. uni.navigateTo({ url: '/pages/login/login' })
  85. return
  86. }
  87. refreshSilent()
  88. })
  89. onPullDownRefresh(() => refreshSilent())
  90. onReachBottom(() => loadMore())
  91. async function fetchList(reset = false) {
  92. if (loading.value) {
  93. uni.stopPullDownRefresh()
  94. return
  95. }
  96. loading.value = true
  97. if (reset) page.value = 1
  98. try {
  99. const res = await inviteApi.getList({
  100. page_no: page.value,
  101. page_size: pageSize
  102. })
  103. const rows = res.list || []
  104. const data = res.data || {}
  105. list.value = reset ? rows : list.value.concat(rows)
  106. total.value = Number(data.total || rows.length)
  107. stats.value = data.stats || {}
  108. } catch (e) {
  109. toast(e.msg || '邀请记录加载失败')
  110. } finally {
  111. loading.value = false
  112. uni.stopPullDownRefresh()
  113. }
  114. }
  115. function refreshSilent() {
  116. fetchList(true)
  117. }
  118. function loadMore() {
  119. if (loading.value || list.value.length >= total.value) return
  120. page.value += 1
  121. fetchList()
  122. }
  123. function formatTime(value) {
  124. if (!value) return ''
  125. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  126. }
  127. function getInitial(name = '') {
  128. return String(name || '友').slice(0, 1)
  129. }
  130. function statusClass(status) {
  131. status = Number(status)
  132. if (status === 1) return 'success'
  133. if (status === 0) return 'pending'
  134. return 'muted'
  135. }
  136. function statusText(status) {
  137. status = Number(status)
  138. if (status === 1) return '已奖励'
  139. if (status === 2) return '已记录'
  140. if (status < 0) return '失败'
  141. return '待发放'
  142. }
  143. function getRecordDesc(item) {
  144. const status = Number(item.status)
  145. if (status === 1) return '好友注册成功,奖励已到账'
  146. if (status === 2) return '已记录,本次未触发奖励'
  147. if (status < 0) return item.remark || '奖励发放失败'
  148. return '等待奖励发放'
  149. }
  150. function goInvite() {
  151. const pages = getCurrentPages()
  152. const index = pages.findIndex((page) => page.route === 'pages/invite/invite')
  153. if (index >= 0 && pages.length - 1 - index > 0) {
  154. uni.navigateBack({ delta: pages.length - 1 - index })
  155. return
  156. }
  157. uni.navigateTo({ url: '/pages/invite/invite' })
  158. }
  159. </script>
  160. <style lang="scss" scoped>
  161. .invite-list-page {
  162. min-height: 100vh;
  163. padding: 24rpx 24rpx calc(52rpx + env(safe-area-inset-bottom));
  164. background: #f4f8fc;
  165. box-sizing: border-box;
  166. }
  167. .summary-card {
  168. padding: 28rpx 28rpx;
  169. border-radius: 28rpx;
  170. color: #ffffff;
  171. background: linear-gradient(135deg, #0ea5e9 0%, #1f8cff 100%);
  172. display: flex;
  173. align-items: center;
  174. justify-content: space-between;
  175. gap: 24rpx;
  176. box-shadow: 0 16rpx 34rpx rgba(14, 165, 233, 0.18);
  177. }
  178. .eyebrow,
  179. .summary-title,
  180. .summary-desc,
  181. .count-num,
  182. .count-label,
  183. .stat-num,
  184. .stat-label,
  185. .name,
  186. .reward-desc,
  187. .time,
  188. .empty-title,
  189. .empty-desc {
  190. display: block;
  191. }
  192. .eyebrow {
  193. opacity: 0.72;
  194. font-size: 20rpx;
  195. font-weight: 900;
  196. line-height: 1;
  197. }
  198. .summary-title {
  199. margin-top: 14rpx;
  200. font-size: 40rpx;
  201. font-weight: 900;
  202. line-height: 1.18;
  203. }
  204. .summary-desc {
  205. margin-top: 12rpx;
  206. opacity: 0.86;
  207. font-size: 24rpx;
  208. line-height: 1.45;
  209. }
  210. .summary-count {
  211. width: 126rpx;
  212. height: 126rpx;
  213. border-radius: 28rpx;
  214. background: rgba(255, 255, 255, 0.16);
  215. display: flex;
  216. flex-direction: column;
  217. align-items: center;
  218. justify-content: center;
  219. flex-shrink: 0;
  220. }
  221. .count-num {
  222. font-size: 42rpx;
  223. font-weight: 900;
  224. line-height: 1;
  225. }
  226. .count-label {
  227. margin-top: 10rpx;
  228. opacity: 0.82;
  229. font-size: 22rpx;
  230. }
  231. .stats-row {
  232. margin-top: 18rpx;
  233. display: grid;
  234. grid-template-columns: repeat(3, minmax(0, 1fr));
  235. gap: 14rpx;
  236. }
  237. .stat-card {
  238. padding: 20rpx 8rpx;
  239. border-radius: 22rpx;
  240. background: #ffffff;
  241. text-align: center;
  242. box-shadow: 0 8rpx 22rpx rgba(31, 52, 86, 0.05);
  243. }
  244. .stat-num {
  245. color: #172033;
  246. font-size: 34rpx;
  247. font-weight: 900;
  248. line-height: 1;
  249. }
  250. .stat-label {
  251. margin-top: 10rpx;
  252. color: #7c8a9b;
  253. font-size: 22rpx;
  254. }
  255. .record-list {
  256. margin-top: 22rpx;
  257. }
  258. .record-card {
  259. margin-bottom: 18rpx;
  260. padding: 24rpx;
  261. border-radius: 24rpx;
  262. background: #ffffff;
  263. display: flex;
  264. gap: 20rpx;
  265. box-shadow: 0 8rpx 22rpx rgba(31, 52, 86, 0.045);
  266. }
  267. .avatar-wrap {
  268. width: 86rpx;
  269. height: 86rpx;
  270. border-radius: 50%;
  271. background: #ecf7ff;
  272. display: flex;
  273. align-items: center;
  274. justify-content: center;
  275. overflow: hidden;
  276. flex-shrink: 0;
  277. }
  278. .avatar {
  279. width: 100%;
  280. height: 100%;
  281. }
  282. .avatar-text {
  283. color: #0b8cff;
  284. font-size: 30rpx;
  285. font-weight: 900;
  286. }
  287. .record-main {
  288. flex: 1;
  289. min-width: 0;
  290. }
  291. .record-head {
  292. display: flex;
  293. align-items: center;
  294. gap: 16rpx;
  295. }
  296. .name {
  297. flex: 1;
  298. min-width: 0;
  299. color: #172033;
  300. font-size: 30rpx;
  301. font-weight: 900;
  302. line-height: 1.25;
  303. overflow: hidden;
  304. text-overflow: ellipsis;
  305. white-space: nowrap;
  306. }
  307. .status {
  308. height: 40rpx;
  309. padding: 0 16rpx;
  310. border-radius: 999rpx;
  311. font-size: 22rpx;
  312. font-weight: 800;
  313. line-height: 40rpx;
  314. flex-shrink: 0;
  315. &.success {
  316. color: #13a86b;
  317. background: #e9fff4;
  318. }
  319. &.pending {
  320. color: #f97316;
  321. background: #fff4e8;
  322. }
  323. &.muted {
  324. color: #7f8896;
  325. background: #f2f5f9;
  326. }
  327. }
  328. .reward-line {
  329. margin-top: 14rpx;
  330. display: flex;
  331. align-items: center;
  332. gap: 12rpx;
  333. }
  334. .reward-chip {
  335. height: 38rpx;
  336. padding: 0 14rpx;
  337. border-radius: 999rpx;
  338. color: #f97316;
  339. background: #fff3e8;
  340. font-size: 22rpx;
  341. font-weight: 900;
  342. line-height: 38rpx;
  343. flex-shrink: 0;
  344. }
  345. .reward-desc {
  346. min-width: 0;
  347. color: #64748b;
  348. font-size: 24rpx;
  349. line-height: 1.35;
  350. overflow: hidden;
  351. text-overflow: ellipsis;
  352. white-space: nowrap;
  353. }
  354. .time {
  355. margin-top: 12rpx;
  356. color: #9aa4b2;
  357. font-size: 22rpx;
  358. }
  359. .empty-state {
  360. margin-top: 72rpx;
  361. padding: 48rpx 34rpx;
  362. border-radius: 28rpx;
  363. background: #ffffff;
  364. display: flex;
  365. flex-direction: column;
  366. align-items: center;
  367. text-align: center;
  368. box-shadow: 0 8rpx 22rpx rgba(31, 52, 86, 0.045);
  369. }
  370. .empty-mark {
  371. width: 96rpx;
  372. height: 96rpx;
  373. border-radius: 30rpx;
  374. color: #0b8cff;
  375. background: #eef8ff;
  376. font-size: 40rpx;
  377. font-weight: 900;
  378. line-height: 96rpx;
  379. }
  380. .empty-title {
  381. margin-top: 26rpx;
  382. color: #172033;
  383. font-size: 32rpx;
  384. font-weight: 900;
  385. line-height: 1;
  386. }
  387. .empty-desc {
  388. margin-top: 16rpx;
  389. color: #7f8896;
  390. font-size: 25rpx;
  391. line-height: 1.45;
  392. }
  393. .empty-action {
  394. width: 230rpx;
  395. height: 72rpx;
  396. margin-top: 28rpx;
  397. border-radius: 999rpx;
  398. color: #ffffff;
  399. background: #0b8cff;
  400. font-size: 26rpx;
  401. font-weight: 900;
  402. line-height: 72rpx;
  403. box-shadow: 0 12rpx 24rpx rgba(11, 140, 255, 0.2);
  404. }
  405. .loading,
  406. .end-text {
  407. padding: 26rpx 0 0;
  408. color: #9aa4b2;
  409. font-size: 24rpx;
  410. text-align: center;
  411. }
  412. </style>