index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
  2. <route lang="json5" type="home">
  3. {
  4. layout: 'tabbar',
  5. style: {
  6. // 'custom' 表示开启自定义导航栏,默认 'default'
  7. navigationStyle: 'custom'
  8. }
  9. }
  10. </route>
  11. <script lang="ts" setup>
  12. // 必须导入需要用到的页面生命周期(即使在当前页面上没有直接使用到)
  13. // eslint-disable-next-line unused-imports/no-unused-imports
  14. import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
  15. import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
  16. import { advList, bannerList, noticeUnread } from '@/api/common'
  17. import { getList } from '@/api/product'
  18. import { toPage } from '@/utils/page'
  19. defineOptions({
  20. name: 'Index', // 首页
  21. })
  22. // 获取屏幕边界到安全区域距离
  23. const systemInfo = uni.getSystemInfoSync()
  24. const safeAreaInsets = systemInfo.safeAreaInsets
  25. // z-paging
  26. const paging = ref(null)
  27. // 类似mixins,如果是页面滚动务必要写这一行,并传入当前ref绑定的paging,注意此处是paging,而非paging.value
  28. useZPaging(paging)
  29. // 轮播图
  30. const current = ref<number>(0)
  31. const swiperList = ref([])
  32. function handleSwiperClick(e: any) {
  33. if (e.item.linkType === 0) {
  34. toPage({ url: e.item.link })
  35. }
  36. else {
  37. toPage({ url: '/pages/webLink/webLink', params: { title: e.item.title, link: e.item.link } })
  38. }
  39. }
  40. // 导航图标
  41. const navIcons = ref([
  42. {
  43. image: '/static/icons/mission-center.png',
  44. title: 'home.missionCenter',
  45. size: '100rpx',
  46. url: '/pages/missionCenter/missionCenter',
  47. },
  48. {
  49. image: '/static/icons/refer-earn.png',
  50. title: 'home.refer&earn',
  51. size: '100rpx',
  52. url: '/pages/referEarn/referEarn',
  53. },
  54. {
  55. image: '/static/icons/vip-membership.png',
  56. title: 'home.vip',
  57. size: '112rpx',
  58. url: '/pages/vipMembership/vipMembership',
  59. },
  60. {
  61. image: '/static/icons/best-sellers.png',
  62. title: 'home.bestSellers',
  63. size: '100rpx',
  64. url: '/pages/bestSellers/bestSellers',
  65. },
  66. {
  67. image: '/static/icons/top-champions.png',
  68. title: 'home.topChampions',
  69. size: '100rpx',
  70. url: '/pages/topChampions/topChampions',
  71. },
  72. ])
  73. // 新品列表
  74. const newProducts = ref<any>([])
  75. async function getNewList() {
  76. const res = await getList({ page: 1, size: 20, sort: 'CREATE_DESC' })
  77. console.log(res)
  78. newProducts.value = res.data.list
  79. }
  80. // 商品列表
  81. const priceTab = ref<string>('300')
  82. // 300Spot 500Spot 1000Spot 2000Spot
  83. const priceTabList = ref([
  84. {
  85. title: 'home.priceTab.300spot',
  86. value: '300',
  87. minPrice: 0,
  88. maxPrice: 300,
  89. },
  90. {
  91. title: 'home.priceTab.500spot',
  92. value: '500',
  93. minPrice: 300,
  94. maxPrice: 500,
  95. },
  96. {
  97. title: 'home.priceTab.1000spot',
  98. value: '1000',
  99. minPrice: 500,
  100. maxPrice: 1000,
  101. },
  102. {
  103. title: 'home.priceTab.2000spot',
  104. value: '2000',
  105. minPrice: 1000,
  106. maxPrice: 2000,
  107. },
  108. {
  109. title: 'home.priceTab.3000spot',
  110. value: '3000',
  111. minPrice: 2000,
  112. maxPrice: 3000,
  113. },
  114. ])
  115. const dataList = ref<any>([])
  116. const isProductListLoading = ref(false) // 商品列表加载状态
  117. function handlePriceTabChange() {
  118. // 获取 tabs 元素位置,确保切换后页面位置在 tabs 位置
  119. uni.createSelectorQuery()
  120. .select('.productList')
  121. .boundingClientRect((rect: any) => {
  122. if (rect) {
  123. uni.createSelectorQuery()
  124. .selectViewport()
  125. .scrollOffset((scrollRes: any) => {
  126. const currentScrollTop = scrollRes?.scrollTop || 0
  127. const tabsTop = currentScrollTop + rect.top
  128. // reload 数据
  129. paging.value?.reload()
  130. // 延迟滚动到 tabs 位置
  131. setTimeout(() => {
  132. uni.pageScrollTo({
  133. scrollTop: tabsTop - 40,
  134. duration: 0,
  135. })
  136. }, 100)
  137. })
  138. .exec()
  139. }
  140. })
  141. .exec()
  142. }
  143. async function queryList(pageNo: number, pageSize: number) {
  144. // 如果是第一页,显示骨架屏
  145. if (pageNo === 1) {
  146. isProductListLoading.value = true
  147. }
  148. try {
  149. const params = {
  150. page: pageNo,
  151. size: pageSize,
  152. sort: 'SALES_DESC',
  153. price: priceTab.value,
  154. }
  155. const res = await getList(params)
  156. paging.value.complete(res.data.list)
  157. }
  158. finally {
  159. if (pageNo === 1) {
  160. isProductListLoading.value = false
  161. }
  162. }
  163. }
  164. async function getBannerList() {
  165. const res = await bannerList({ page: 1, size: 20 })
  166. swiperList.value = res.data.list
  167. }
  168. const unread = ref(0)
  169. const isPageLoading = ref(true) // 页面加载状态
  170. // 创建一个通用方法来处理所有初始数据加载
  171. async function loadData() {
  172. try {
  173. isPageLoading.value = true
  174. await Promise.all([
  175. getUnread(),
  176. getNewList(),
  177. getBannerList(),
  178. ])
  179. }
  180. finally {
  181. isPageLoading.value = false
  182. }
  183. }
  184. async function getUnread() {
  185. try {
  186. const res = await noticeUnread()
  187. if (res.code === '200') {
  188. unread.value = Number(res.data) || 0
  189. }
  190. }
  191. catch {}
  192. }
  193. const curtain = reactive({
  194. show: false,
  195. img: '',
  196. link: '',
  197. linkType: '', // IN OUT
  198. title: '',
  199. })
  200. async function getCurtain() {
  201. try {
  202. const res = await advList({ advType: 'INDEX' })
  203. if (res.code === '200' && res.data.length) {
  204. curtain.show = true
  205. curtain.img = res.data[0].advImage
  206. curtain.link = res.data[0].link
  207. curtain.linkType = res.data[0].linkType
  208. curtain.title = res.data[0].title
  209. }
  210. }
  211. catch {}
  212. }
  213. function curtainClick() {
  214. if (curtain.linkType === 'IN') {
  215. toPage({ url: curtain.link })
  216. }
  217. else {
  218. toPage({ url: '/pages/webLink/webLink', params: { title: curtain.title, link: curtain.link } })
  219. }
  220. }
  221. onLoad(async () => {
  222. getCurtain()
  223. await loadData()
  224. })
  225. onShow(() => {
  226. getUnread()
  227. })
  228. </script>
  229. <template>
  230. <z-paging ref="paging" v-model="dataList" :auto-scroll-to-top-when-reload="false" :auto-clean-list-when-reload="false" use-page-scroll @query="queryList" @on-refresh="loadData">
  231. <template #top>
  232. <view
  233. class="flex items-center justify-between bg-white pb-40rpx pl-42rpx pr-34rpx pt-26rpx"
  234. :style="{ paddingTop: `${safeAreaInsets?.top + 13}px` }"
  235. >
  236. <image src="/static/header-logo.png" class="h-44rpx w-275rpx" />
  237. <view class="flex items-center">
  238. <wd-badge :model-value="0">
  239. <image
  240. src="/static/icons/search.png"
  241. class="mr-20rpx h-40rpx w-40rpx"
  242. @click="toPage({ url: '/pages/search/search' })"
  243. />
  244. </wd-badge>
  245. <wd-badge :model-value="unread" :max="99">
  246. <image
  247. src="/static/icons/notifications.png"
  248. class="h-40rpx w-40rpx"
  249. @click="toPage({ url: '/pages/notifications/notifications' })"
  250. />
  251. </wd-badge>
  252. </view>
  253. </view>
  254. </template>
  255. <!-- 页面加载时显示骨架屏 -->
  256. <template v-if="isPageLoading">
  257. <!-- 轮播图骨架屏 -->
  258. <wd-skeleton
  259. :row-col="[{ height: '400rpx' }]"
  260. animation="gradient"
  261. />
  262. <view class="px-24rpx pb-24rpx">
  263. <!-- 导航图标区域骨架屏 -->
  264. <view class="pb-22rpx pt-24rpx">
  265. <wd-skeleton
  266. :row-col="[
  267. [
  268. { width: '100rpx', height: '100rpx', type: 'circle' },
  269. { width: '100rpx', height: '100rpx', type: 'circle' },
  270. { width: '100rpx', height: '100rpx', type: 'circle' },
  271. { width: '100rpx', height: '100rpx', type: 'circle' },
  272. { width: '100rpx', height: '100rpx', type: 'circle' },
  273. ],
  274. [
  275. { width: '60rpx', height: '24rpx' },
  276. { width: '60rpx', height: '24rpx' },
  277. { width: '60rpx', height: '24rpx' },
  278. { width: '60rpx', height: '24rpx' },
  279. { width: '60rpx', height: '24rpx' },
  280. ],
  281. ]"
  282. animation="gradient"
  283. />
  284. </view>
  285. <!-- 新品区域骨架屏 -->
  286. <view class="mb-32rpx">
  287. <wd-skeleton
  288. :row-col="[
  289. { width: '120rpx', height: '32rpx', marginBottom: '16rpx' }, // 标题
  290. [
  291. { width: '260rpx', height: '260rpx' },
  292. { width: '260rpx', height: '260rpx', marginLeft: '16rpx' },
  293. { width: '260rpx', height: '260rpx', marginLeft: '16rpx' },
  294. ],
  295. ]"
  296. animation="gradient"
  297. />
  298. </view>
  299. <!-- 价格分类标签骨架屏 -->
  300. <view class="mb-20rpx">
  301. <wd-skeleton
  302. :row-col="[
  303. [
  304. { width: '80rpx', height: '36rpx' },
  305. { width: '80rpx', height: '36rpx', marginLeft: '20rpx' },
  306. { width: '80rpx', height: '36rpx', marginLeft: '20rpx' },
  307. { width: '80rpx', height: '36rpx', marginLeft: '20rpx' },
  308. { width: '80rpx', height: '36rpx', marginLeft: '20rpx' },
  309. ],
  310. ]"
  311. animation="gradient"
  312. />
  313. </view>
  314. <!-- 商品列表骨架屏 -->
  315. <view class="grid grid-cols-2 gap-20rpx">
  316. <wd-skeleton
  317. v-for="i in 6"
  318. :key="i"
  319. :row-col="[
  320. { height: '340rpx' }, // 商品图片
  321. { width: '180rpx', height: '40rpx', marginTop: '10rpx' }, // 商品名称
  322. [
  323. { width: '80rpx', height: '24rpx' }, // 价格
  324. { width: '60rpx', height: '20rpx' }, // 销量
  325. ],
  326. ]"
  327. animation="gradient"
  328. />
  329. </view>
  330. </view>
  331. </template>
  332. <!-- 实际内容 -->
  333. <template v-else>
  334. <wd-swiper
  335. v-model:current="current"
  336. :list="swiperList"
  337. value-key="image"
  338. autoplay
  339. indicator
  340. indicator-position="bottom-right"
  341. image-mode="aspectFill"
  342. height="388rpx"
  343. @click="handleSwiperClick"
  344. />
  345. <view class="px-24rpx pb-24rpx">
  346. <view class="flex items-end justify-between pb-22rpx pt-24rpx">
  347. <view
  348. v-for="(item, index) in navIcons"
  349. :key="index"
  350. class="flex flex-col items-center"
  351. @click="toPage({ url: item.url })"
  352. >
  353. <image :src="item.image" :style="`width: ${item.size}; height: ${item.size};`" />
  354. <view class="mt-14rpx whitespace-pre-line text-center text-22rpx text-#898989 font-bold">
  355. {{ $t(item.title) }}
  356. </view>
  357. </view>
  358. </view>
  359. <view v-if="newProducts.length">
  360. <view class="mb-16rpx text-32rpx">
  361. {{ $t('home.news') }}
  362. </view>
  363. <scroll-view scroll-x class="whitespace-nowrap">
  364. <view class="flex items-center gap-16rpx" style="min-width: max-content;">
  365. <Product
  366. v-for="(item, index) in newProducts"
  367. :key="index"
  368. :title-font-size="18"
  369. :item="item"
  370. class="shrink-0"
  371. @item-click="toPage({ url: '/pages/productDetail/productDetail', params: { productId: item.productId } })"
  372. />
  373. </view>
  374. </scroll-view>
  375. </view>
  376. <view class="productList">
  377. <wd-sticky :offset-top="0">
  378. <view class="tabs-container">
  379. <wd-tabs v-model="priceTab" slidable="always" :line-width="0" :line-height="0" @click="handlePriceTabChange">
  380. <template v-for="item in priceTabList" :key="item">
  381. <wd-tab :title="$t(item.title)" :name="item.value" />
  382. </template>
  383. </wd-tabs>
  384. </view>
  385. </wd-sticky>
  386. <!-- Tab切换时的商品列表骨架屏 -->
  387. <view v-if="isProductListLoading" class="grid grid-cols-2 gap-20rpx">
  388. <wd-skeleton
  389. v-for="i in 6"
  390. :key="i"
  391. :row-col="[
  392. { height: '340rpx', borderRadius: '12rpx' }, // 商品图片
  393. { width: '180rpx', height: '40rpx', marginTop: '10rpx' }, // 商品名称
  394. [
  395. { width: '80rpx', height: '24rpx' }, // 价格
  396. { width: '60rpx', height: '20rpx' }, // 销量
  397. ],
  398. ]"
  399. animation="gradient"
  400. />
  401. </view>
  402. <!-- 实际商品列表 -->
  403. <view v-else class="grid grid-cols-2 gap-20rpx">
  404. <Product
  405. v-for="(item, index) in dataList"
  406. :key="index"
  407. width="100%"
  408. :height="340"
  409. :item="item"
  410. @item-click="toPage({ url: '/pages/productDetail/productDetail', params: { productId: item.productId } })"
  411. />
  412. </view>
  413. </view>
  414. </view>
  415. </template>
  416. <wd-curtain v-model="curtain.show" :src="curtain.img" :to="curtain.link" close-position="bottom" :width="280" @click="curtainClick" />
  417. </z-paging>
  418. <!-- 在页面最下方添加占位视图,高度等于 tabBar 的高度 -->
  419. <!-- <view class="edgeInsetBottom" /> -->
  420. </template>
  421. <style lang="scss" scoped>
  422. :deep(.productList) {
  423. .wd-tabs {
  424. background: none;
  425. .wd-tabs__nav {
  426. background: none;
  427. height: 72rpx;
  428. }
  429. .wd-tabs__nav-item {
  430. padding-left: 0 !important;
  431. height: 72rpx;
  432. }
  433. }
  434. .wd-sticky__container[style*='position: fixed'] {
  435. .tabs-container {
  436. width: 100vw;
  437. margin-left: -24rpx;
  438. padding: 28rpx 24rpx 4rpx;
  439. background: rgba(255, 255, 255, 0.85);
  440. backdrop-filter: blur(10px);
  441. -webkit-backdrop-filter: blur(10px);
  442. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
  443. }
  444. }
  445. }
  446. </style>