You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

175 lines
5.4 KiB

  1. <template>
  2. <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
  3. <h1 class="text-2xl sm:text-3xl font-bold text-slate-900 mb-2">账单明细</h1>
  4. <p class="text-sm text-slate-500 mb-8">查看你的点数变动记录</p>
  5. <!-- 账单统计 -->
  6. <div class="grid grid-cols-3 gap-4 mb-8">
  7. <div class="bg-white rounded-xl p-4 shadow-sm border border-slate-100 text-center">
  8. <p class="text-xs text-slate-400">累计充值</p>
  9. <p class="mt-1 text-xl font-bold text-green-600">{{ summary.totalRecharge }}</p>
  10. </div>
  11. <div class="bg-white rounded-xl p-4 shadow-sm border border-slate-100 text-center">
  12. <p class="text-xs text-slate-400">累计消费</p>
  13. <p class="mt-1 text-xl font-bold text-slate-900">{{ summary.totalConsume }}</p>
  14. </div>
  15. <div class="bg-white rounded-xl p-4 shadow-sm border border-slate-100 text-center">
  16. <p class="text-xs text-slate-400">累计奖励</p>
  17. <p class="mt-1 text-xl font-bold text-indigo-600">{{ summary.totalReward }}</p>
  18. </div>
  19. </div>
  20. <!-- Tab 筛选 -->
  21. <div class="flex gap-2 overflow-x-auto pb-2 mb-4">
  22. <button
  23. v-for="tab in billTabs"
  24. :key="tab.key"
  25. :class="[
  26. 'shrink-0 px-4 py-2 text-sm rounded-lg transition-colors cursor-pointer',
  27. activeType === tab.key
  28. ? 'bg-indigo-500 text-white font-medium'
  29. : 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50',
  30. ]"
  31. @click="activeType = tab.key"
  32. >
  33. {{ tab.label }}
  34. </button>
  35. </div>
  36. <!-- 加载中 -->
  37. <div v-if="loading" class="flex justify-center py-20">
  38. <svg class="w-8 h-8 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
  39. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
  40. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
  41. </svg>
  42. </div>
  43. <!-- 账单列表 -->
  44. <div v-else-if="bills.length > 0" class="bg-white rounded-xl shadow-sm border border-slate-100 divide-y divide-slate-100">
  45. <div
  46. v-for="bill in bills"
  47. :key="bill.id"
  48. class="flex items-center justify-between px-5 py-4 hover:bg-slate-50/50 transition-colors"
  49. >
  50. <div class="min-w-0 flex-1">
  51. <p class="text-sm font-medium text-slate-800 truncate">{{ bill.remark || bill.type_name || '账单记录' }}</p>
  52. <p class="mt-0.5 text-xs text-slate-400">{{ formatDate(bill.created_at) }}</p>
  53. </div>
  54. <div class="shrink-0 text-right ml-4">
  55. <p :class="[
  56. 'text-sm font-semibold',
  57. bill.points > 0 ? 'text-green-600' : bill.points < 0 ? 'text-slate-700' : 'text-slate-400',
  58. ]">
  59. {{ bill.points > 0 ? '+' : '' }}{{ bill.points }}
  60. </p>
  61. <p class="text-xs text-slate-400">余额 {{ bill.balance }}</p>
  62. </div>
  63. </div>
  64. </div>
  65. <!-- 空状态 -->
  66. <div v-else class="text-center py-20">
  67. <svg class="w-16 h-16 mx-auto text-slate-200 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  68. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
  69. </svg>
  70. <p class="text-sm text-slate-500">暂无账单记录</p>
  71. </div>
  72. <!-- 加载更多 -->
  73. <div v-if="hasMore" class="flex justify-center mt-6">
  74. <button
  75. class="px-6 py-2.5 text-sm text-indigo-600 bg-indigo-50 hover:bg-indigo-100 rounded-lg transition-colors cursor-pointer"
  76. @click="loadMore"
  77. >
  78. 加载更多
  79. </button>
  80. </div>
  81. </div>
  82. </template>
  83. <script setup>
  84. useSeoMeta({
  85. title: '账单明细 - 奇想宇宙',
  86. description: '查看账户点数变动明细',
  87. })
  88. const { isLogin } = useUser()
  89. const { post } = useApi()
  90. const billTabs = [
  91. { label: '全部', key: 'all' },
  92. { label: '充值', key: 'recharge' },
  93. { label: '消费', key: 'consume' },
  94. { label: '奖励', key: 'reward' },
  95. ]
  96. const activeType = ref('all')
  97. const loading = ref(true)
  98. const bills = ref([])
  99. const page = ref(1)
  100. const hasMore = ref(false)
  101. const summary = reactive({
  102. totalRecharge: 0,
  103. totalConsume: 0,
  104. totalReward: 0,
  105. })
  106. function formatDate(dateStr) {
  107. if (!dateStr) return ''
  108. return String(dateStr).substring(0, 10)
  109. }
  110. async function loadBills(reset = false) {
  111. if (reset) {
  112. page.value = 1
  113. bills.value = []
  114. }
  115. loading.value = true
  116. try {
  117. const res = await post('/user/getbills', {
  118. type: activeType.value === 'all' ? '' : activeType.value,
  119. page: page.value,
  120. page_size: 20,
  121. client: 1,
  122. })
  123. if (res.code === 0) {
  124. const list = res.list || res.data || []
  125. if (reset) {
  126. bills.value = list
  127. } else {
  128. bills.value.push(...list)
  129. }
  130. hasMore.value = list.length >= 20
  131. // 更新统计
  132. if (res.data?.summary) {
  133. summary.totalRecharge = res.data.summary.recharge || 0
  134. summary.totalConsume = Math.abs(res.data.summary.consume || 0)
  135. summary.totalReward = res.data.summary.reward || 0
  136. }
  137. }
  138. } catch {
  139. // 忽略
  140. } finally {
  141. loading.value = false
  142. }
  143. }
  144. function loadMore() {
  145. page.value++
  146. loadBills(false)
  147. }
  148. watch(activeType, () => {
  149. loadBills(true)
  150. })
  151. onMounted(() => {
  152. if (isLogin.value) {
  153. loadBills(true)
  154. }
  155. })
  156. </script>