day1
导学
创建
pages.json 和 tabBar案例
<!--src/pages/my/my.vue-->
<template>
<view>
</view>
</template>
<script>
export default {
data() {
return {}
},
methods: {
}
}
</script>
<style scoped lang="scss">
</style>
// src/pages.json
{
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "我的",
"enablePullDownRefresh": true
}
}
],
"tabBar": {
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "static/tabs/user_default.png",
"selectedIconPath": "static/tabs/user_selected.png"
}
],
"color": "",
"selectedColor": "#27BA9B",
"backgroundColor": ""
},
"globalStyle": {
// "navigationBarTextStyle": "black",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
// "navigationBarBackgroundColor": "#F8F8F8",
"navigationBarBackgroundColor": "#27BA9B",
"backgroundColor": "#F8F8F8"
}
}
uniapp和原生微信小程序的区别
<template>
<swiper class="banner" indicator-dots circular
:autoplay="false">
<swiper-item v-for="item in pictures"
:key="item.id">
<image
@tap="onPreviewImage(item.url)" :src="item.url">
</image>
</swiper-item>
</swiper>
</template>
<script>
export default {
data() {
return {
title: 'Hello',
pictures: [
{id: '1', url: "https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_1.jpg"},
{id: '2', url: "https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_2.jpg"},
{id: '3', url: "https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_3.jpg"},
{id: '4', url: "https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_4.jpg"},
{id: '5', url: "https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_5.jpg"},
]
}
},
onLoad() {
},
methods: {
onPreviewImage(url) {
uni.previewImage( {
urls: this.pictures.map(v => v.url),
current: url
})
}
},
}
</script>
<style>
.banner, .banner image {
width: 750rpx;
height: 750rpx;
}
</style>
命令行创建uni-app项目
vscode开发uniapp
pnpm i -D @types/wechat-miniprogram @uni-helper/uni-app-types
// tsconfig.json
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"lib": [
"esnext",
"dom"
],
"types": [
"@dcloudio/types",
"@types/wechat-miniprogram",
"@uni-helper/uni-app-types"
]
},
"vueCompilerOptions": {
"experimentalRuntimeMode": "runtime-uni-app"
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}
拉取小兔鲜儿模板
git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git heima-shop
安装uni-ui组件库
pnpm i @dcloudio/uni-ui
// pages.json
{
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
// 其他内容
pages: [
// ...
]
}
pnpm i @uni-helper/uni-ui-types
pinia状态持久化
请求和上传文件拦截器
<script setup lang="ts">
import {useMemberStore} from '@/stores'
import {http} from '@/utils/http'
const memberStore = useMemberStore()
const getData = async () => {
uni.request({
method: 'GET',
url: '/home/banner',
})
}
</script>
<template>
<view class="my">
<view>会员信息:{{memberStore.profile}}</view>
<button
@tap="
memberStore.setProfile({
nickname: '黑马先锋',
token: '123456',
})
"
size="mini"
plain
type="primary"
>
保存用户信息
</button>
<button
@tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用户信息
</button>
<button @tap = "getData"
size = "mini"
plain
type = "primary" > 测试请求 < /button>
</view>
</template>
<style lang="scss">
</style>
import {useMemberStore} from '@/stores'
const baseURL = 'https://pcapi-xiaotuxian-front-devtest.itheima.net'
const httpInterceptor = {
invoke(options: UniApp.RequestOptions) {
if (!options.url.startsWith('http')) {
options.url = baseURL + options.url
}
options.timeout = 10000
options.header = {
...options.header,
'source-client': 'miniapp',
}
const memberStore = useMemberStore()
const token = memberStore.profile?.token
if (token) {
options.header.Authorization = token
}
console.log(options)
},
}
uni.addInterceptor('request', httpInterceptor)
uni.addInterceptor('uploadFile', httpInterceptor)
请求函数封装
<!--src/pages/my/my.vue-->
<script setup lang="ts">
import {useMemberStore} from '@/stores'
import {http} from '@/utils/http'
const memberStore = useMemberStore()
const getData = async () => {
const res = await http<string[]>({
method: 'GET',
url: '/home/banner',
header: {}
})
console.log('请求成功: ', res)
}
</script>
interface
Data < T > {
code: string
msg: string
result: T
}
请求函数封装 - 失败情况
<!--src/pages/my/my.vue-->
<script setup lang="ts">
import {useMemberStore} from '@/stores'
import {http} from '@/utils/http'
const memberStore = useMemberStore()
const getData = async () => {
const res = await http<string[]>({
method: 'GET',
url: "",
header: {},
})
console.log('请求成功: ', res)
}
</script>
interface
Data < T > {
code: string
msg: string
result: T
}
export const http =
<
T > (options: UniApp.RequestOptions)
=>
{
return new Promise < Data < T >> ((resolve, reject) => {
uni.request({
...options,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data
as
Data < T >
)
} else if (res.statusCode === 401) {
const memberStore = useMemberStore()
memberStore.clearProfile()
uni.navigateTo({url: '/pages/login/login'})
reject(res)
} else {
uni.showToast({
icon: 'none',
title: (res.data as Data < T >).msg || '请求错误'
})
reject(res)
}
},
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误, 换个网络试试'
})
reject(err)
}
})
})
}
首页 自定义导航栏
<script setup lang="ts">
</script>
<template>
<view class="navbar">
<!-- logo文字 -->
<view class="logo">
<image class="logo-image" src="@/static/images/logo.png"></image>
<text class="logo-text">新鲜 · 亲民 · 快捷</text>
</view>
<!-- 搜索条 -->
<view class="search">
<text class="icon-search">搜索商品</text>
<text class="icon-scan"></text>
</view>
</view>
</template>
<style lang="scss">
.navbar {
background - image: url(@/static/images/navigator_bg.png);
background-size: cover;
position: relative;
display: flex;
flex-direction: column;
padding-top: 20px;
.logo {
display: flex;
align-items: center;
height: 64rpx;
padding-left: 30rpx;
padding-top: 20rpx;
.logo-image {
width: 166rpx;
height: 39rpx;
}
.logo-text {
flex: 1;
line-height: 28rpx;
color: #fff;
margin: 2rpx 0 0 20rpx;
padding-left: 20rpx;
border-left: 1rpx solid #fff;
font-size: 26rpx;
}
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx 0 26rpx;
height: 64rpx;
margin: 16rpx 20rpx;
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.5);
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
.icon-scan {
font-size: 30rpx;
padding: 15rpx;
}
}
</style>
<script setup lang="ts">
const {safeAreaInsets} = uni.getSystemInfoSync()
console.log(safeAreaInsets)
</script>
<template>
<view class="navbar"
:style="{paddingTop:safeAreaInsets?.top+'px'}">
<!-- logo文字 -->
<view class="logo">
<image class="logo-image" src="@/static/images/logo.png"></image>
<text class="logo-text">新鲜 · 亲民 · 快捷</text>
</view>
<!-- 搜索条 -->
<view class="search">
<text class="icon-search">搜索商品</text>
<text class="icon-scan"></text>
</view>
</view>
</template>
<style lang="scss">
.navbar {
background - image: url(@/static/images/navigator_bg.png);
background-size: cover;
position: relative;
display: flex;
flex-direction: column;
padding-top: 20px;
.logo {
display: flex;
align-items: center;
height: 64rpx;
padding-left: 30rpx;
padding-top: 20rpx;
.logo-image {
width: 166rpx;
height: 39rpx;
}
.logo-text {
flex: 1;
line-height: 28rpx;
color: #fff;
margin: 2rpx 0 0 20rpx;
padding-left: 20rpx;
border-left: 1rpx solid #fff;
font-size: 26rpx;
}
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx 0 26rpx;
height: 64rpx;
margin: 16rpx 20rpx;
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.5);
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
.icon-scan {
font-size: 30rpx;
padding: 15rpx;
}
}
</style>
{
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
// 去掉默认导航栏
"navigationStyle": "custom",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "首页"
}
},
// ...
]
}
day2
轮播图
通用组件自动导入
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from "@/pages/index/componets/CustomNavbar.vue";
import XtxSwiper from "@/components/XtxSwiper.vue";
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<!-- 自定义轮播图 -->
<XtxSwiper/>
<view class="index">index</view>
</template>
<style lang="scss">
</style>
// src/pages.json
{
// 组件自动引入规则
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
// 自动导入src/components的组件(名称Xtx开头)
"^Xtx(.*)": "@/components/Xtx$1.vue"
}
},
}
<!--静态结构: src/components/XtxSwiper.vue-->
<script setup lang="ts">
import {ref} from 'vue'
const activeIndex = ref(0)
</script>
<template>
<view class="carousel">
<swiper
:circular="true" :autoplay="false" :interval="3000">
<swiper-item>
<navigator url="/pages/index/index" hover-class="none" class="navigator">
<image
mode="aspectFill"
class="image"
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_1.jpg"
></image>
</navigator>
</swiper-item>
<swiper-item>
<navigator url="/pages/index/index" hover-class="none" class="navigator">
<image
mode="aspectFill"
class="image"
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_2.jpg"
></image>
</navigator>
</swiper-item>
<swiper-item>
<navigator url="/pages/index/index" hover-class="none" class="navigator">
<image
mode="aspectFill"
class="image"
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/slider_3.jpg"
></image>
</navigator>
</swiper-item>
</swiper>
<!-- 指示点 -->
<view class="indicator">
<text
v-for="(item, index) in 3"
:key="item"
class="dot"
:class="{active: index === activeIndex}"
>
</text>
</view>
</view>
</template>
<style lang="scss">
:host {
display: block;
height: 280rpx;
}
.carousel {
height: 100%;
position: relative;
overflow: hidden;
transform: translateY(0);
background-color: #efefef;
.indicator {
position: absolute;
left: 0;
right: 0;
bottom: 16rpx;
display: flex;
justify-content: center;
.dot {
width: 30rpx;
height: 6rpx;
margin: 0 8rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.4);
}
.active {
background-color: #fff;
}
}
.navigator,
.image {
width: 100%;
height: 100%;
}
}
</style>
import XtxSwiper from './XtxSwiper.vue'
declare module 'vue' {
export interface GlobalComponents {
XtxSwiper: typeof XtxSwiper
}
}
轮播图指示点
<!--静态结构: src/components/XtxSwiper.vue-->
<script setup lang="ts">
import {ref} from 'vue'
const activeIndex = ref(0)
const onChange: UniHelper.SwiperOnChange = (ev) => {
activeIndex.value = ev.detail!.current
}
</script>
<template>
<view class="carousel">
<swiper
:circular="true" :autoplay="false" :interval="3000" @change="onChange">
</swiper>
</view>
...
<template>
获取轮播图数据
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/componets/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI} from "@/services/home";
import {onLoad} from "@dcloudio/uni-app";
import {ref} from "vue";
const bannerList = ref([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI();
console.log(res)
bannerList.value = res.result
}
onLoad(() => {
getHomeBannerData()
})
</script>
import {http} from "@/utils/http";
export const getHomeBannerAPI = (distributionSite = 1) => {
return http({
method: 'GET',
url: '/home/banner',
data: {
distributionSite
}
})
}
类型定义和渲染
<!--静态结构: src/components/XtxSwiper.vue-->
<script setup lang="ts">
import {ref} from 'vue'
import type {BannerItem} from "@/types/home";
const activeIndex = ref(0)
const onChange: UniHelper.SwiperOnChange = (ev) => {
activeIndex.value = ev.detail!.current
}
const props = defineProps
<
{
list: BannerItem[]
}>()
</script>
<template>
<view class="carousel">
<swiper
:circular="true" :autoplay="false" :interval="3000" @change="onChange">
<swiper-item v-for="item in list"
:key="item.id">
<navigator url="/pages/index/index" hover-class="none" class="navigator">
<image
mode="aspectFill"
class="image"
:src="item.imgUrl"
>
</image>
</navigator>
</swiper-item>
</swiper>
<!-- 指示点 -->
<view class="indicator">
<text
v-for="(item, index) in list"
:key="item.id"
class="dot"
:class="{active: index === activeIndex}"
>
</text>
</view>
</view>
</template>
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/componets/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI} from "@/services/home";
import {onLoad} from "@dcloudio/uni-app";
import {ref} from "vue";
import type {BannerItem} from "@/types/home";
const bannerList = ref
<BannerItem
[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI();
console.log(res)
bannerList.value = res.result
}
onLoad(() => {
getHomeBannerData()
})
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList"/>
<view class="index">index</view>
</template>
import {http} from "@/utils/http";
import type {BannerItem} from "@/types/home";
export const getHomeBannerAPI = (distributionSite = 1) => {
return http<BannerItem[]>({
method: 'GET',
url: '/home/banner',
data: {
distributionSite
}
})
}
export type BannerItem = {
hrefUrl: string
id: string
imgUrl: string
type: number
}
总结
前台分类
组件封装
<!--src/pages/index/components/CategoryPanel.vue-->
<script setup lang="ts">
</script>
<template>
<view class="category">
<navigator
class="category-item"
hover-class="none"
url="/pages/index/index"
v-for="item in 10"
:key="item"
>
<image
class="icon"
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/nav_icon_1.png"
></image>
<text class="text">居家</text>
</navigator>
</view>
</template>
<style lang="scss">
.category {
margin: 20rpx 0 0;
padding: 10rpx 0;
display: flex;
flex-wrap: wrap;
min-height: 328rpx;
.category-item {
width: 150rpx;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
box-sizing: border-box;
.icon {
width: 100rpx;
height: 100rpx;
}
.text {
font-size: 26rpx;
color: #666;
}
}
}
</style>
<!--src/pages/index/index.vue-->
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList" />
<!-- 分类面板 -->
<CategoryPanel/>
</template>
<style lang="scss">
page {
background - color: #f7f7f7;
}
</style>
获取数据
import {http} from '@/utils/http'
import type {BannerItem} from '@/types/home'
export const getHomeCategoryAPI = () => {
return http({
method: 'GET',
url: '/home/category/mutli'
})
}
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
const bannerList = ref
<BannerItem
[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
console.log(res)
bannerList.value = res.result
}
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
console.log(res)
}
onLoad(() => {
getHomeBannerData()
getHomeCategoryData()
})
</script>
类型定义和渲染
<!--src/pages/index/components/CategoryPanel.vue-->
<script setup lang="ts">
import type {CategoryItem} from "@/types/home";
const props = defineProps
<
{
list: CategoryItem[]
}>()
</script>
<template>
<view class="category">
<navigator
class="category-item"
hover-class="none"
url="/pages/index/index"
v-for="item in list"
:key="item.id"
>
<image
class="icon"
:src="item.icon"
>
</image>
<text class="text">{{item.name}}</text>
</navigator>
</view>
</template>
<style lang="scss">
.category {
margin: 20rpx 0 0;
padding: 10rpx 0;
display: flex;
flex-wrap: wrap;
min-height: 328rpx;
.category-item {
width: 150rpx;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
box-sizing: border-box;
.icon {
width: 100rpx;
height: 100rpx;
}
.text {
font-size: 26rpx;
color: #666;
}
}
}
</style>
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem, CategoryItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
const bannerList = ref
<BannerItem
[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
console.log(res)
bannerList.value = res.result
}
const categoryList = ref
<CategoryItem
[]>([])
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
console.log(res)
categoryList.value = res.result
}
onLoad(() => {
getHomeBannerData()
getHomeCategoryData()
})
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList" />
<!-- 分类面板 -->
<CategoryPanel
:list="categoryList" />
</template>
import {http} from '@/utils/http'
import type {BannerItem, CategoryItem} from '@/types/home'
export const getHomeCategoryAPI = () => {
return http<CategoryItem[]>({
method: 'GET',
url: '/home/category/mutli',
})
}
export type CategoryItem = {
icon: string
id: string
name: string
}
热门推荐
<!--src/pages/index/components/HotPanel.vue-->
<script setup lang="ts">
import type {HotItem} from "@/types/home";
const props = defineProps
<
{
list: HotItem[]
}>()
</script>
<template>
<!-- 推荐专区 -->
<view class="panel hot">
<view class="item" v-for="item in list"
:key="item.id">
<view class="title">
<text class="title-text">{{item.title}}</text>
<text class="title-desc">{{item.alt}}</text>
</view>
<navigator hover-class="none" url="/pages/hot/hot" class="cards">
<image
v-for="src in item.pictures"
:key="src"
class="image"
mode="aspectFit"
:src="src"
>
</image>
</navigator>
</view>
</view>
</template>
<style lang="scss">
.hot {
display: flex;
flex-wrap: wrap;
min-height: 508rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
background-color: #fff;
.title {
display: flex;
align-items: center;
padding: 24rpx 24rpx 0;
font-size: 32rpx;
color: #262626;
position: relative;
.title-desc {
font-size: 24rpx;
color: #7f7f7f;
margin-left: 18rpx;
}
}
.item {
display: flex;
flex-direction: column;
width: 50%;
height: 254rpx;
border-right: 1rpx solid #eee;
border-top: 1rpx solid #eee;
.title {
justify-content: start;
}
&:nth-child(2n) {
border-right: 0 none;
}
&:nth-child(-n + 2) {
border-top: 0 none;
}
.image {
width: 150rpx;
height: 150rpx;
}
}
.cards {
flex: 1;
padding: 15rpx 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
import HotPanel from "@/pages/index/components/HotPanel.vue";
const bannerList = ref
<BannerItem
[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
const categoryList = ref
<CategoryItem
[]>([])
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
categoryList.value = res.result
}
const hotList = ref
<HotItem
[]>([])
const getHomeHotData = async () => {
const res = await getHomeHotAPI()
hotList.value = res.result
}
onLoad(() => {
getHomeBannerData()
getHomeCategoryData()
getHomeHotData()
})
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList"/>
<!-- 分类面板 -->
<CategoryPanel
:list="categoryList"/>
<!-- 热门推荐 -->
<HotPanel
:list="hotList"/>
</template>
import {http} from '@/utils/http'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
export const getHomeHotAPI = () => {
return http<HotItem[]>({
method: 'GET',
url: '/home/hot/mutli'
})
}
export type HotItem = {
alt: string
id: string
pictures: string[]
target: string
title: string
type: string
}
猜你喜欢
组件封装
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
import HotPanel from "@/pages/index/components/HotPanel.vue";
import XtxGuess from "@/components/XtxGuess.vue";
const bannerList = ref
<BannerItem
[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
const categoryList = ref
<CategoryItem
[]>([])
const getHomeCategoryData = async () => {
const res = await getHomeCategoryAPI()
categoryList.value = res.result
}
const hotList = ref
<HotItem
[]>([])
const getHomeHotData = async () => {
const res = await getHomeHotAPI()
hotList.value = res.result
}
onLoad(() => {
getHomeBannerData()
getHomeCategoryData()
getHomeHotData()
})
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<scroll-view class="scroll-view" scroll-y>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList"/>
<!-- 分类面板 -->
<CategoryPanel
:list="categoryList"/>
<!-- 热门推荐 -->
<HotPanel
:list="hotList"/>
<!-- 猜你喜欢 -->
<XtxGuess/>
</scroll-view>
</template>
<style lang="scss">
page {
background - color: #f7f7f7;
height: 100%;
display: flex;
flex-direction: column;
}
.scroll-view {
flex: 1;
}
</style>
import XtxSwiper from '../components/XtxSwiper.vue'
import XtxGuess from "@/components/XtxGuess.vue";
declare module 'vue' {
export interface GlobalComponents {
XtxSwiper: typeof XtxSwiper,
XtxGuess: typeof XtxGuess
}
}
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
</script>
<template>
<!-- 猜你喜欢 -->
<view class="caption">
<text class="text">猜你喜欢</text>
</view>
<view class="guess">
<navigator
class="guess-item"
v-for="item in 10"
:key="item"
:url="`/pages/goods/goods?id=4007498`"
>
<image
class="image"
mode="aspectFill"
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_big_1.jpg"
></image>
<view class="name">德国THORE男表 超薄手表男士休闲简约夜光石英防水直径40毫米</view>
<view class="price">
<text class="small">¥</text>
<text>899.00</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</template>
<style lang="scss">
:host {
display: block;
}
.caption {
display: flex;
justify-content: center;
line-height: 1;
padding: 36rpx 0 40rpx;
font-size: 32rpx;
color: #262626;
.text {
display: flex;
justify-content: center;
align-items: center;
padding: 0 28rpx 0 30rpx;
&::before,
&::after {
content: '';
width: 20rpx;
height: 20rpx;
background-image: url(@/static/images/bubble.png);
background-size: contain;
margin: 0 10rpx;
}
}
}
.guess {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 20rpx;
.guess-item {
width: 345rpx;
padding: 24rpx 20rpx 20rpx;
margin-bottom: 20rpx;
border-radius: 10rpx;
overflow: hidden;
background-color: #fff;
}
.image {
width: 304rpx;
height: 304rpx;
}
.name {
height: 75rpx;
margin: 10rpx 0;
font-size: 26rpx;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
line-height: 1;
padding-top: 4rpx;
color: #cf4444;
font-size: 26rpx;
}
.small {
font-size: 80%;
}
}
.loading-text {
text - align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
}
</style>
获取数据
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from "@/services/home";
import {onMounted} from "vue"
const getHomeGoodsGuessLikeData = async () => {
const res = await getHomeGoodsGuessLikeAPI()
console.log(res)
}
onMounted(() => {
getHomeGoodsGuessLikeData()
})
</script>
import {http} from '@/utils/http'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
export const getHomeGoodsGuessLikeAPI = () => {
return http({
method: 'GET',
url: '/home/goods/guessLike'
})
}
类型定义 和 列表渲染
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from "@/services/home";
import {onMounted, ref} from "vue"
import type {GuessItem} from "@/types/home";
const guessList = ref
<GuessItem
[]>([])
const getHomeGoodsGuessLikeData = async () => {
const res = await getHomeGoodsGuessLikeAPI()
guessList.value = res.result.items
}
onMounted(() => {
getHomeGoodsGuessLikeData()
})
</script>
<template>
<!-- 猜你喜欢 -->
<view class="caption">
<text class="text">猜你喜欢</text>
</view>
<view class="guess">
<navigator
class="guess-item"
v-for="item in guessList"
:key="item.id"
:url="`/pages/goods/goods?id=4007498`"
>
<image
class="image"
mode="aspectFill"
:src="item.picture"
>
</image>
<view class="name">{{item.name}}</view>
<view class="price">
<text class="small">¥</text>
<text>{{item.price}}</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</template>
export type PageResult<T> = {
items: T[]
counts: number
page: number
pages: number
pageSize: number
}
export type GuessItem = {
desc: string
discount: number
id: string
name: string
orderNum: number
picture: string
price: number
}
import {http} from '@/utils/http'
import type {BannerItem, CategoryItem, GuessItem, HotItem} from '@/types/home'
import type {PageResult} from '@/types/global'
export const getHomeGoodsGuessLikeAPI = () => {
return http<PageResult<GuessItem>>({
method: 'GET',
url: '/home/goods/guessLike',
})
}
分页准备
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
import HotPanel from '@/pages/index/components/HotPanel.vue'
import XtxGuess from '@/components/XtxGuess.vue'
import type {XtxGuessInstance} from "@/types/component";
const guessRef = ref
<XtxGuessInstance>()
const onScrollToLower = () => {
guessRef.value?.getMore()
}
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<scroll-view class="scroll-view"
@scrolltolower="onScrollToLower" scroll-y>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList"/>
<!-- 分类面板 -->
<CategoryPanel
:list="categoryList"/>
<!-- 热门推荐 -->
<HotPanel
:list="hotList"/>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef"/>
</scroll-view>
</template>
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from "@/services/home";
import {onMounted, ref} from "vue"
import type {GuessItem} from "@/types/home";
defineExpose({
getMore: getHomeGoodsGuessLikeData
})
</script>
import 'vue'
import XtxSwiper from '../components/XtxSwiper.vue'
import XtxGuess from '@/components/XtxGuess.vue'
declare module 'vue' {
export interface GlobalComponents {
XtxSwiper: typeof XtxSwiper
XtxGuess: typeof XtxGuess
}
}
export type XtxGuessInstance = InstanceType<typeof XtxGuess>
分页加载
export type PageParams = {
page?: number
pageSize?: number
}
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from "@/services/home";
import {onMounted, ref} from "vue"
import type {GuessItem} from "@/types/home";
import type {PageParams} from "@/types/global";
const pageParams: Required
<PageParams>= {
page: 1,
pageSize: 10
}
const guessList = ref
<GuessItem
[]>([])
const getHomeGoodsGuessLikeData = async () => {
const res = await getHomeGoodsGuessLikeAPI(pageParams)
guessList.value.push(...res.result.items)
pageParams.page++
}
</script>
export const getHomeGoodsGuessLikeAPI = (data?: PageParams) => {
return http<PageResult<GuessItem>>({
method: 'GET',
url: '/home/goods/guessLike',
data
})
}
分页条件
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from "@/services/home";
import {onMounted, ref} from "vue"
import type {GuessItem} from "@/types/home";
import type {PageParams} from "@/types/global";
const pageParams: Required
<PageParams>= {
page: 1,
pageSize: 10
}
const guessList = ref
<GuessItem
[]>([])
const finish = ref(false)
const getHomeGoodsGuessLikeData = async () => {
if (finish.value) {
return uni.showToast({icon: 'none', title: '没有更多数据了'})
}
const res = await getHomeGoodsGuessLikeAPI(pageParams)
guessList.value.push(...res.result.items)
if (pageParams.page < res.result.pages) {
pageParams.page++
} else {
finish.value = true
}
}
onMounted(() => {
getHomeGoodsGuessLikeData()
})
defineExpose({
getMore: getHomeGoodsGuessLikeData
})
</script>
<template>
<!-- 猜你喜欢 -->
<view class="caption">
<text class="text">猜你喜欢</text>
</view>
<view class="guess">
<navigator
class="guess-item"
v-for="item in guessList"
:key="item.id"
:url="`/pages/goods/goods?id=4007498`"
>
<image
class="image"
mode="aspectFill"
:src="item.picture"
>
</image>
<view class="name">{{item.name}}</view>
<view class="price">
<text class="small">¥</text>
<text>{{item.price}}</text>
</view>
</navigator>
</view>
<view class="loading-text">{{finish ? '没有更多了' : '正在加载...'}}</view>
</template>
下拉刷新
下拉刷新
<!--src/pages/index/index.vue-->
<script setup lang="ts">
import CustomNavbar from '@/pages/index/components/CustomNavbar.vue'
import XtxSwiper from '@/components/XtxSwiper.vue'
import {getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI} from '@/services/home'
import {onLoad} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type {BannerItem, CategoryItem, HotItem} from '@/types/home'
import CategoryPanel from '@/pages/index/components/CategoryPanel.vue'
import HotPanel from '@/pages/index/components/HotPanel.vue'
import XtxGuess from '@/components/XtxGuess.vue'
import type {XtxGuessInstance} from '@/types/component'
const isTriggered = ref(false)
const onRefresherrefresh = async () => {
isTriggered.value = true
await Promise.all([getHomeBannerData(), getHomeCategoryData(), getHomeHotData()])
isTriggered.value = false
}
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<scroll-view
refresher-enabled
@refresherrefresh="onRefresherrefresh"
:refresher-triggered="isTriggered"
class="scroll-view"
@scrolltolower="onScrollToLower"
scroll-y
>
<!-- 自定义轮播图 -->
<XtxSwiper
:list="bannerList" />
<!-- 分类面板 -->
<CategoryPanel
:list="categoryList" />
<!-- 热门推荐 -->
<HotPanel
:list="hotList" />
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef"/>
</scroll-view>
</template>
猜你喜欢数据
<!--src/pages/index/index.vue-->
<script setup lang="ts">
const isTriggered = ref(false)
const onRefresherrefresh = async () => {
isTriggered.value = true
guessRef.value?.resetData()
await Promise.all([
getHomeBannerData(), getHomeCategoryData(),
getHomeHotData(), guessRef.value?.getMore()
])
isTriggered.value = false
}
</script>
<!--src/components/XtxGuess.vue-->
<script setup lang="ts">
import {getHomeGoodsGuessLikeAPI} from '@/services/home'
import {onMounted, ref} from 'vue'
import type {GuessItem} from '@/types/home'
import type {PageParams} from '@/types/global'
const pageParams: Required
<PageParams>= {
page: 1,
pageSize: 10,
}
const resetData = () => {
pageParams.page = 1
guessList.value = []
finish.value = false
}
defineExpose({
resetData,
getMore: getHomeGoodsGuessLikeData,
})
</script>
day3
首页 - 骨架屏
<!--src/pages/index/index.vue-->
<script setup lang="ts">
// ...
import PageSkeleton from "@/pages/index/components/PageSkeleton.vue";
// ...
// 是否加载中标记
const isLoading = ref(false)
// 页面加载
onLoad(async () => {
isLoading.value = true
await Promise.all([
getHomeBannerData(),
getHomeCategoryData(),
getHomeHotData()
])
isLoading.value = false
})
// ...
</script>
<template>
<!-- 自定义导航栏 -->
<CustomNavbar/>
<scroll-view
refresher-enabled
@refresherrefresh="onRefresherrefresh"
:refresher-triggered="isTriggered"
class="scroll-view"
@scrolltolower="onScrollToLower"
scroll-y
>
<!-- 数据未加载时显示: 骨架屏 -->
<PageSkeleton v-if="isLoading"/>
<template v-else>
<!-- 自定义轮播图 -->
<XtxSwiper :list="bannerList"/>
<!-- 分类面板 -->
<CategoryPanel :list="categoryList"/>
<!-- 热门推荐 -->
<HotPanel :list="hotList"/>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef"/>
</template>
</scroll-view>
</template>
// ...
<!--src/pages/index/components/PageSkeleton.vue-->
<template>
<view class="sk-container">
<scroll-view :refresher-enabled="true" :scroll-y="true" class="scroll-view scroll-view">
<view is="components/XtxSwiper">
<view class="carousel XtxSwiper--carousel">
<swiper :circular="true" :interval="3000" :current="0" :autoplay="false">
<swiper-item style="position: absolute; width: 100%; height: 100%; transform: translate(0%, 0px) translateZ(0px);">
<navigator class="navigator XtxSwiper--navigator" hover-class="none">
<image class="image XtxSwiper--image sk-image" mode="aspectFill"></image>
</navigator>
</swiper-item>
</swiper>
<view class="indicator XtxSwiper--indicator">
<text class="dot XtxSwiper--dot active XtxSwiper--active"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
</view>
</view>
</view>
<view is="pages/index/components/CategoryPanel">
<view class="category CategoryPanel--category">
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-979 sk-text">居家</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-430 sk-text">锦鲤</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-352 sk-text">服饰</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-550 sk-text">母婴</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-772 sk-text">个护</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-247 sk-text">严选</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-512 sk-text">数码</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-956 sk-text">运动</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-19 sk-text">杂项</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-105 sk-text">品牌</text>
</navigator>
</view>
</view>
<view is="pages/index/components/HotPanel">
<view class="panel HotPanel--panel hot HotPanel--hot">
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-278 sk-text">特惠推荐</text>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-515 sk-text">精选全攻略</text>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-463 sk-text">爆款推荐</text>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-998 sk-text">最受欢迎</text>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-982 sk-text">一站买全</text>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-757 sk-text">精心优选</text>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-722 sk-text">新鲜好物</text>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-632 sk-text">生活加分项</text>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
热门推荐
准备工作
<!--src/pages/index/components/HotPanel.vue-->
<script setup lang="ts">
//
</script>
<template>
<!-- 推荐专区 -->
<view class="panel hot">
<view class="item" v-for="item in list" :key="item.id">
<!-- -->
<navigator hover-class="none" :url="`/pages/hot/hot?type=${item.type}`" class="cards">
<image
v-for="src in item.pictures"
:key="src"
class="image"
mode="aspectFit"
:src="src"
></image>
</navigator>
</view>
</view>
</template>
<!--src/pages/hot/hot.vue-->
<script setup lang="ts">
const hotMap = [
{type: '1', title: '特惠推荐', url: '/hot/preference'},
{type: '2', title: '爆款推荐', url: '/hot/inVogue'},
{type: '3', title: '一站买全', url: '/hot/oneStop'},
{type: '4', title: '新鲜好物', url: '/hot/new'},
]
const query = defineProps
<
{
type: string
}>()
const curHotMap = hotMap.find(v => v.type === query.type)
uni.setNavigationBarTitle({title: curHotMap!.title})
</script>
<template>
<view class="viewport">
<!-- 推荐封面图 -->
<view class="cover">
<image
src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-20/84abb5b1-8344-49ae-afc1-9cb932f3d593.jpg"
></image>
</view>
<!-- 推荐选项 -->
<view class="tabs">
<text class="text active">抢先尝鲜</text>
<text class="text">新品预告</text>
</view>
<!-- 推荐列表 -->
<scroll-view scroll-y class="scroll-view">
<view class="goods">
<navigator
hover-class="none"
class="navigator"
v-for="goods in 10"
:key="goods"
:url="`/pages/goods/goods?id=`"
>
<image
class="thumb"
src="https://yanxuan-item.nosdn.127.net/5e7864647286c7447eeee7f0025f8c11.png"
></image>
<view class="name ellipsis">不含酒精,使用安心爽肤清洁湿巾</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">29.90</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</scroll-view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
padding: 180rpx 0 0;
position: relative;
}
.cover {
width: 750rpx;
height: 225rpx;
border-radius: 0 0 40rpx 40rpx;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
}
.scroll-view {
flex: 1;
}
.tabs {
display: flex;
justify-content: space-evenly;
height: 100rpx;
line-height: 90rpx;
margin: 0 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
box-shadow: 0 4rpx 5rpx rgba(200, 200, 200, 0.3);
color: #333;
background-color: #fff;
position: relative;
z-index: 9;
.text {
margin: 0 20rpx;
position: relative;
}
.active {
&::after {
content: '';
width: 40rpx;
height: 4rpx;
transform: translate(-50%);
background-color: #27ba9b;
position: absolute;
left: 50%;
bottom: 24rpx;
}
}
}
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 20rpx 20rpx;
.navigator {
width: 345rpx;
padding: 20rpx;
margin-top: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
.thumb {
width: 305rpx;
height: 305rpx;
}
.name {
height: 88rpx;
font-size: 26rpx;
}
.price {
line-height: 1;
color: #cf4444;
font-size: 30rpx;
}
.symbol {
font-size: 70%;
}
.decimal {
font-size: 70%;
}
}
.loading-text {
text - align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0 50rpx;
}
</style>
获取数据
<script setup lang="ts">
const getHotRecommendData = async () => {
const res = await getHotRecommendAPI(curHotMap
!
.
url
)
console.log(res)
}
onLoad(() => {
getHotRecommendData()
})
</script>
import {http} from "@/utils/http";
import type {PageParams} from "@/types/global";
type HotParams = PageParams & { subType: string }
export const getHotRecommendAPI = (
url: string,
data?: HotParams
) => {
return http({
method: 'GET',
url, data
})
}
类型定义
import type {PageResult, GoodsItem} from './global'
export type HotResult = {
id: string
bannerPicture: string
title: string
subTypes: SubTypeItem[]
}
export type SubTypeItem = {
id: string
title: string
goodsItems: PageResult<GoodsItem>
}
import type {GoodsItem} from '@/types/global'
export type GuessItem = GoodsItem
import {http} from '@/utils/http'
import type {PageParams} from '@/types/global'
import type {HotResult} from '@/types/hot'
type HotParams = PageParams & { subType: string }
export const getHotRecommendAPI = (url: string, data?: HotParams) => {
return http<HotResult>({
method: 'GET',
url,
data,
})
}
export type GoodsItem = {
desc: string
discount: number
id: string
name: string
orderNum: number
picture: string
price: number
}
页面渲染和tab交互
<script setup lang="ts">
const bannerPicture = ref('')
const subTypes = ref < SubTypeItem[] > ([])
const activeIndex = ref(0)
const getHotRecommendData = async () => {
const res = await getHotRecommendAPI(curHotMap
!
.
url
)
bannerPicture.value = res.result.bannerPicture
subTypes.value = res.result.subTypes
}
onLoad(() => {
getHotRecommendData()
})
</script>
<template>
<view class="viewport">
<view class="cover">
<image :src="bannerPicture"></image>
</view>
<view class="tabs">
<text v-for="(item,idx) in subTypes"
:key="item.id" class="text"
@tap="activeIndex = idx"
:class="{active: activeIndex === idx }"
>{{ item.title }}
</text>
</view>
<scroll-view v-for="(item,idx) in subTypes" v-show="activeIndex===idx" :key="item.id" scroll-y
class="scroll-view">
<view class="goods">
<navigator
hover-class="none"
class="navigator"
v-for="goods in item.goodsItems.items"
:key="goods.id"
:url="`/pages/goods/goods?id=${goods.id}`"
>
<image
class="thumb"
:src="goods.picture"
></image>
<view class="name ellipsis">{{ goods.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods.price }}</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</scroll-view>
</view>
</template>
分页加载
<script setup lang="ts">
const onScrollToLower = async () => {
const curSubTypes = subTypes.value[activeIndex.value]
curSubTypes.goodsItems.page++
const res = await getHotRecommendAPI(curHotMap
!
.
url, {
subType: curSubTypes.id,
page: curSubTypes.goodsItems.page,
pageSize: curSubTypes.goodsItems.pageSize,
}
)
const newSubTypes = res.result.subTypes[activeIndex.value]
curSubTypes.goodsItems.items.push(...newSubTypes.goodsItems.items)
}
</script>
<template>
<view class="viewport">
///
///
<scroll-view
v-for="(item, idx) in subTypes"
v-show="activeIndex === idx"
:key="item.id"
@scrolltolower="onScrollToLower"
scroll-y
class="scroll-view"
>
///
</scroll-view>
</view>
</template>
分页条件
<script setup lang="ts">
const bannerPicture = ref('')
const subTypes = ref < (SubTypeItem & {finish? : boolean})[] > ([])
const activeIndex = ref(0)
const getHotRecommendData = async () => {
const res = await getHotRecommendAPI(curHotMap
!
.
url, {
page: import.meta.env.DEV ? 30 : 1,
pageSize: 10,
}
)
bannerPicture.value = res.result.bannerPicture
subTypes.value = res.result.subTypes
}
const onScrollToLower = async () => {
const curSubTypes = subTypes.value[activeIndex.value]
if (curSubTypes.goodsItems.page < curSubTypes.goodsItems.pages) {
curSubTypes.goodsItems.page++
} else {
curSubTypes.finish = true
return uni.showToast({title: '没有更多了', icon: 'none'})
}
}
</script>
<template>
<view class="viewport">
<scroll-view
v-for="(item, idx) in subTypes"
v-show="activeIndex === idx"
:key="item.id"
@scrolltolower="onScrollToLower"
scroll-y
class="scroll-view"
>
...
<view class="loading-text"> {{ item.finish ? '没有更多数据了' : '正在加载...' }}</view>
</scroll-view>
</view>
</template>
商品分类
准备工作
<script setup lang="ts">
import {getHomeBannerAPI} from "@/services/home";
import {ref} from "vue";
import type {BannerItem} from "@/types/home";
import {onLoad} from "@dcloudio/uni-app";
const bannerList = ref < BannerItem[] > ([])
const getBannerData = async () => {
const res = await getHomeBannerAPI(2)
bannerList.value = res.result
}
onLoad(() => {
getBannerData()
})
</script>
<template>
<view class="viewport">
<view class="search">
<view class="input">
<text class="icon-search">女靴</text>
</view>
</view>
<view class="categories">
<scroll-view class="primary" scroll-y>
<view v-for="(item, index) in 10" :key="item" class="item" :class="{ active: index === 0 }">
<text class="name"> 居家</text>
</view>
</scroll-view>
<scroll-view class="secondary" scroll-y>
<XtxSwiper class="banner" :list="bannerList"/>
<view class="panel" v-for="item in 3" :key="item">
<view class="title">
<text class="name">宠物用品</text>
<navigator class="more" hover-class="none">全部</navigator>
</view>
<view class="section">
<navigator
v-for="goods in 4"
:key="goods"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=`"
>
<image
class="image"
src="https://yanxuan-item.nosdn.127.net/674ec7a88de58a026304983dd049ea69.jpg"
></image>
<view class="name ellipsis">木天蓼逗猫棍</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">16.00</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
}
.search {
padding: 0 30rpx 20rpx;
background-color: #fff;
.input {
display: flex;
align-items: center;
justify-content: space-between;
height: 64rpx;
padding-left: 26rpx;
color: #8b8b8b;
font-size: 28rpx;
border-radius: 32rpx;
background-color: #f3f4f4;
}
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
.categories {
flex: 1;
min-height: 400rpx;
display: flex;
}
.primary {
overflow: hidden;
width: 180rpx;
flex: none;
background-color: #f6f6f6;
.item {
display: flex;
justify-content: center;
align-items: center;
height: 96rpx;
font-size: 26rpx;
color: #595c63;
position: relative;
&::after {
content: '';
position: absolute;
left: 42rpx;
bottom: 0;
width: 96rpx;
border-top: 1rpx solid #e3e4e7;
}
}
.active {
background-color: #fff;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 8rpx;
height: 100%;
background-color: #27ba9b;
}
}
}
.primary .item:last-child::after,
.primary .active::after {
display: none;
}
.secondary {
background-color: #fff;
.carousel {
height: 200rpx;
margin: 0 30rpx 20rpx;
border-radius: 4rpx;
overflow: hidden;
}
.panel {
margin: 0 30rpx 0rpx;
}
.title {
height: 60rpx;
line-height: 60rpx;
color: #333;
font-size: 28rpx;
border-bottom: 1rpx solid #f7f7f8;
.more {
float: right;
padding-left: 20rpx;
font-size: 24rpx;
color: #999;
}
}
.more {
&::after {
font-family: 'erabbit' !important;
content: '\e6c2';
}
}
.section {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 20rpx 0;
.goods {
width: 150rpx;
margin: 0rpx 30rpx 20rpx 0;
&:nth-child(3n) {
margin-right: 0;
}
image {
width: 150rpx;
height: 150rpx;
}
.name {
padding: 5rpx;
font-size: 22rpx;
color: #333;
}
.price {
padding: 5rpx;
font-size: 18rpx;
color: #cf4444;
}
.number {
font-size: 24rpx;
margin-left: 2rpx;
}
}
}
}
</style>
渲染一级分类和tab交互
import {http} from '@/utils/http'
import type {CategoryTopItem} from "@/types/category";
export const getCategoryTopAPI = () => {
return http<CategoryTopItem[]>({
method: 'GET',
url: '/category/top',
})
}
import type {GoodsItem} from './global'
export type CategoryTopItem = {
children: CategoryChildItem[]
id: string
imageBanners: string[]
name: string
picture: string
}
export type CategoryChildItem = {
goods: GoodsItem[]
id: string
name: string
picture: string
}
<script setup lang="ts">
import {getHomeBannerAPI} from "@/services/home";
import {ref} from "vue";
import type {BannerItem} from "@/types/home";
import {onLoad} from "@dcloudio/uni-app";
import {getCategoryTopAPI} from "@/services/category";
import type {CategoryTopItem} from "@/types/category";
const categoryList = ref < CategoryTopItem[] > ([])
const activeIndex = ref(0)
const getCategoryTopData = async () => {
const res = await getCategoryTopAPI()
categoryList.value = res.result
}
onLoad(() => {
getBannerData()
getCategoryTopData()
})
</script>
<template>
<view class="viewport">
<view class="categories">
<scroll-view class="primary" scroll-y>
<view v-for="(item, index) in categoryList"
:key="item.id" class="item"
:class="{ active: index === activeIndex }"
@tap="activeIndex=index">
<text class="name"> {{ item.name }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
渲染二级分类和商品
<script setup lang="ts">
const subCategoryList = computed(() => {
return categoryList.value[activeIndex.value]?.children || []
})
</script>
<template>
<view class="viewport">
<view class="categories">
<scroll-view class="secondary" scroll-y>
<XtxSwiper class="banner" :list="bannerList"/>
<view class="panel" v-for="item in subCategoryList" :key="item.id">
<view class="title">
<text class="name">{{ item.name }}</text>
<navigator class="more" hover-class="none">全部</navigator>
</view>
<view class="section">
<navigator
v-for="goods in item.goods"
:key="goods.id"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=${goods.id}`"
>
<image
class="image"
:src="goods.picture"
></image>
<view class="name ellipsis">{{ goods.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods.price }}</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
骨架屏
<template name="skeleton">
<view class="sk-container">
<view class="viewport viewport">
<view class="search search">
<view class="input input">
<text class="icon-search sk-transparent sk-text-14-2857-26 sk-text sk-pseudo sk-pseudo-circle">
女靴
</text>
</view>
</view>
<view class="categories categories">
<scroll-view :scroll-y="true" class="primary primary">
<view class="item active sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-864 sk-text">居家</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-287 sk-text">美食</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-139 sk-text">服饰</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-396 sk-text">母婴</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-87 sk-text">个护</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-719 sk-text">严选</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-928 sk-text">数码</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-993 sk-text">运动</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-593 sk-text">杂项</text>
</view>
</scroll-view>
<scroll-view :scroll-y="true" class="secondary secondary">
<view is="components/XtxSwiper" class="banner banner">
<view class="carousel XtxSwiper--carousel">
<swiper :circular="true" :interval="3000" :current="0" :autoplay="false">
<swiper-item
style="position: absolute; width: 100%; height: 100%; transform: translate(0%, 0px) translateZ(0px);">
<navigator class="navigator XtxSwiper--navigator" hover-class="none">
<image class="image XtxSwiper--image sk-image" mode="aspectFill"></image>
</navigator>
</swiper-item>
<swiper-item
style="position: absolute; width: 100%; height: 100%; transform: translate(100%, 0px) translateZ(0px);">
<navigator class="navigator XtxSwiper--navigator" hover-class="none">
<image class="image XtxSwiper--image sk-image" mode="aspectFill"></image>
</navigator>
</swiper-item>
</swiper>
<view class="indicator XtxSwiper--indicator">
<text class="dot XtxSwiper--dot active XtxSwiper--active"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
</view>
</view>
</view>
<view class="panel panel">
<view class="title title">
<text class="name sk-transparent sk-text-26-7857-722 sk-text">居家生活用品</text>
<navigator
class="more sk-transparent sk-text-30-3571-467 sk-text sk-pseudo sk-pseudo-circle"
hover-class="none">全部
</navigator>
</view>
<view class="section section">
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-58 sk-text">
梅乃宿梅酒720毫升
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-243 sk-text">168.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-711 sk-text">
法国年份雅文邑700毫升
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-807 sk-text">1480.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-886 sk-text">
多米尼加陈年朗姆酒700毫升
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-231 sk-text">238.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-637 sk-text">
川味牛肉辣椒酱190克
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-834 sk-text">38.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-778 sk-text">全新升级,四川酸辣粉195克*6杯
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-90 sk-text">69.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-342 sk-text">极光限定
珠光蓝珐琅锅
</view>
<view class="price price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-25 sk-text">199.00</text>
</view>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
</navigator>
<navigator class="goods goods" hover-class="none">
<image class="image sk-image"></image>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<style>
.sk-transparent {
color: transparent !important;
}
.sk-text-14-2857-26 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 37.9167rpx;
position: relative !important;
}
.sk-text {
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
color: transparent !important;
background-repeat: repeat-y !important;
}
.sk-text-14-2857-864 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-287 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-139 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-396 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-87 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-719 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-928 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-993 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-14-2857-593 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 35.0000rpx;
position: relative !important;
}
.sk-text-26-7857-722 {
background-image: linear-gradient(transparent 26.7857%, #EEEEEE 0%, #EEEEEE 73.2143%, transparent 0%) !important;
background-size: 100% 58.3333rpx;
position: relative !important;
}
.sk-text-30-3571-467 {
background-image: linear-gradient(transparent 30.3571%, #EEEEEE 0%, #EEEEEE 69.6429%, transparent 0%) !important;
background-size: 100% 58.3333rpx;
position: relative !important;
}
.sk-text-14-2857-58 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-opacity {
opacity: 0 !important;
}
.sk-text-14-2857-243 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-text-14-2857-711 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-text-14-2857-807 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-text-14-2857-886 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-text-14-2857-231 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-text-14-2857-637 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-text-14-2857-834 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-text-14-2857-778 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-text-14-2857-90 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-text-14-2857-342 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 29.1667rpx;
position: relative !important;
}
.sk-text-14-2857-25 {
background-image: linear-gradient(transparent 14.2857%, #EEEEEE 0%, #EEEEEE 85.7143%, transparent 0%) !important;
background-size: 100% 32.0833rpx;
position: relative !important;
}
.sk-image {
background: #EFEFEF !important;
}
.sk-pseudo::before, .sk-pseudo::after {
background: #EFEFEF !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}
.sk-pseudo-rect::before, .sk-pseudo-rect::after {
border-radius: 0 !important;
}
.sk-pseudo-circle::before, .sk-pseudo-circle::after {
border-radius: 50% !important;
}
.sk-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
</style>
<script setup lang="ts">
import {getHomeBannerAPI} from '@/services/home'
import {computed, ref} from 'vue'
import type {BannerItem} from '@/types/home'
import {onLoad} from '@dcloudio/uni-app'
import {getCategoryTopAPI} from '@/services/category'
import type {CategoryTopItem} from '@/types/category'
import PageSkeleton from "@/pages/category/components/PageSkeleton.vue";
const isFinish = ref(false)
onLoad(async () => {
await Promise.all([getBannerData(), getCategoryTopData()])
isFinish.value = true
})
</script>
<template>
<view class="viewport" v-if="isFinish">
</view>
<PageSkeleton v-else/>
</template>
商品详情