TreeDetailView.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <template>
  2. <div class="tree-detail-page">
  3. <van-nav-bar title="许愿树详情" left-text="返回" left-arrow @click-left="$router.back()" />
  4. <div v-if="tree">
  5. <div class="tree-banner" :style="{ background: treeGradient }">
  6. <span class="banner-emoji">{{ treeEmoji }}</span>
  7. <h2 class="banner-name">{{ tree.name }}</h2>
  8. </div>
  9. <div class="tree-header">
  10. <h2>{{ tree.name }}</h2>
  11. <p class="tree-addr"><van-icon name="location-o" /> {{ tree.address }}</p>
  12. <p class="tree-desc">{{ tree.description }}</p>
  13. <div class="tree-stats">
  14. <span>💬 {{ tree.totalWishes }} 个愿望</span>
  15. <span v-if="tree.distance != null" class="tree-dist">
  16. <van-tag :type="tree.isInRange ? 'success' : 'warning'" round size="medium">
  17. {{ tree.isInRange ? '可许愿' : '' }} {{ fmtDist(tree.distance) }}
  18. </van-tag>
  19. </span>
  20. </div>
  21. <van-button
  22. type="primary"
  23. block
  24. round
  25. :disabled="!locationStore.located"
  26. @click="goMakeWish"
  27. >
  28. 去许愿 🙏
  29. </van-button>
  30. </div>
  31. <van-tabs v-model:active="activeTab" sticky>
  32. <van-tab title="公开愿望">
  33. <van-pull-refresh v-model="refreshing" @refresh="loadWishes">
  34. <wish-card
  35. v-for="wish in wishes"
  36. :key="wish.id"
  37. :wish="wish"
  38. @click="goWish(wish.id)"
  39. />
  40. <van-empty v-if="wishes.length === 0" description="还没有人在这里许愿,快来第一个!" />
  41. </van-pull-refresh>
  42. </van-tab>
  43. </van-tabs>
  44. </div>
  45. <van-loading v-else class="loading" />
  46. <ChatWidget :tree-id="tree?.id" />
  47. </div>
  48. </template>
  49. <script setup lang="ts">
  50. import { ref, computed, onMounted } from 'vue'
  51. import { useRoute, useRouter } from 'vue-router'
  52. import { useLocationStore } from '@/stores/location'
  53. import { fetchTreeDetail } from '@/api/tree'
  54. import { fetchTreeWishes } from '@/api/wish'
  55. import WishCard from '@/components/WishCard.vue'
  56. import ChatWidget from '@/components/ChatWidget.vue'
  57. import { getTreeGradient, getTreeEmoji } from '@/utils/theme'
  58. import type { WishingTree } from '@/mock/data'
  59. import type { Wish } from '@/mock/data'
  60. const route = useRoute()
  61. const router = useRouter()
  62. const locationStore = useLocationStore()
  63. const tree = ref<WishingTree | null>(null)
  64. const wishes = ref<Wish[]>([])
  65. const activeTab = ref(0)
  66. const refreshing = ref(false)
  67. const treeGradient = computed(() => getTreeGradient(tree.value?.id || 1))
  68. const treeEmoji = computed(() => getTreeEmoji(tree.value?.id || 1))
  69. async function loadWishes() {
  70. const id = Number(route.params.id)
  71. const result = await fetchTreeWishes(id)
  72. wishes.value = result.list
  73. refreshing.value = false
  74. }
  75. function goMakeWish() {
  76. router.push({ path: '/make-wish', query: { treeId: route.params.id } })
  77. }
  78. function fmtDist(m: number) {
  79. return m >= 1000 ? (m / 1000).toFixed(1) + 'km' : m + 'm'
  80. }
  81. function goWish(id: number) {
  82. router.push(`/wish/${id}`)
  83. }
  84. onMounted(async () => {
  85. const id = Number(route.params.id)
  86. if (!locationStore.located) {
  87. await locationStore.refreshLocation()
  88. }
  89. tree.value = await fetchTreeDetail(id, locationStore.lng ?? undefined, locationStore.lat ?? undefined)
  90. loadWishes()
  91. })
  92. </script>
  93. <style scoped>
  94. .tree-detail-page {
  95. min-height: 100vh;
  96. background: #f7f8fa;
  97. }
  98. .tree-banner {
  99. width: 100%;
  100. height: 200px;
  101. display: flex;
  102. flex-direction: column;
  103. align-items: center;
  104. justify-content: center;
  105. gap: 8px;
  106. color: #fff;
  107. }
  108. .banner-emoji {
  109. font-size: 48px;
  110. text-shadow: 0 1px 4px rgba(0,0,0,0.2);
  111. }
  112. .banner-name {
  113. font-size: 20px;
  114. font-weight: 600;
  115. color: #fff;
  116. margin: 0;
  117. text-shadow: 0 1px 3px rgba(0,0,0,0.2);
  118. }
  119. .tree-header {
  120. padding: 16px;
  121. background: #fff;
  122. margin-bottom: 8px;
  123. }
  124. .tree-header h2 {
  125. font-size: 20px;
  126. margin-bottom: 8px;
  127. }
  128. .tree-addr {
  129. font-size: 13px;
  130. color: #666;
  131. margin-bottom: 10px;
  132. display: flex;
  133. align-items: center;
  134. gap: 4px;
  135. }
  136. .tree-desc {
  137. font-size: 14px;
  138. color: #555;
  139. line-height: 1.6;
  140. margin-bottom: 12px;
  141. }
  142. .tree-stats {
  143. display: flex;
  144. justify-content: space-between;
  145. align-items: center;
  146. margin-bottom: 16px;
  147. font-size: 13px;
  148. }
  149. .tree-dist {
  150. margin-left: 8px;
  151. }
  152. .loading {
  153. margin: 100px auto;
  154. }
  155. </style>