黑马程序员uni-app 小兔鲜儿 项目及bug记录(上)(1):https://developer.aliyun.com/article/1548503
骨架屏
我没有生成出猜你喜欢/XtxGuess的骨架屏!!
- 再生成一遍
骨架屏显示逻辑代码
<script setup lang="ts"> import { getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI } from '@/services/home' import CoustomNavbar from './components/CustomNavbar.vue' import { ref } from 'vue' import { onLoad } from '@dcloudio/uni-app' import type { BannerItem, CategoryItem, HotItem } from '@/types/home' import CategoryPanel from './components/CategoryPanel.vue' import HotPanel from './components/HotPanel.vue' import type { XtxGuessInstance } from '@/types/component' import PageSkeleton from './components/PageSkeleton.vue' // 获取轮播图数据 const bannerList = ref<BannerItem[]>([]) const getHomeBannerDate = async () => { const res = await getHomeBannerAPI() bannerList.value = res.result } // 获取前台数据 const categoryList = ref<CategoryItem[]>([]) const getHomeCategoryDate = async () => { const res = await getHomeCategoryAPI() categoryList.value = res.result } const hotList = ref<HotItem[]>([]) // 获取热门推荐数据 const getgetHomeHotDate = async () => { const res = await getHomeHotAPI() hotList.value = res.result } // 获取猜你喜欢组件实例 const guessRef = ref<XtxGuessInstance>() // 滚动触底事件 const onScrolltolower = () => { guessRef.value?.getMore() } // 是否加载中标记 const isLoading = ref(false) // 加载设置 onLoad(async () => { isLoading.value = true await Promise.all([getHomeBannerDate(), getHomeCategoryDate(), getgetHomeHotDate()]) isLoading.value = false }) // 下拉刷新状态 const isTriggered = ref(false) // 自定义下拉刷新被触发 const onRefresherrefresh = async () => { console.log('被下拉') // 开启动画 isTriggered.value = true // 重置猜你喜欢组件数据 guessRef.value?.resetData() // 加载数据 await Promise.all([ getHomeBannerDate(), getHomeCategoryDate(), getgetHomeHotDate(), guessRef.value?.getMore(), ]) // 关闭动画 isTriggered.value = false } </script> <template> <!-- 自定义导航栏 --> <CoustomNavbar /> <!-- 使用这个来实现上面的不会滚动 --> <!-- <scroll-view scroll-y @scrolltolower="onScrolltolower"> --> <scroll-view refresher-enabled @refresherrefresh="onRefresherrefresh" @scrolltolower="onScrolltolower" :refresher-triggered="isTriggered" class="scroll-view" scroll-y > <PageSkeleton v-if="isLoading" /> <template v-else> <XtxSwiper :list="bannerList" /> <CategoryPanel :list="categoryList" /> <!-- 热门推荐 --> <HotPanel :list="hotList" /> <!-- 猜你喜欢,已经在pages.json中实现了自动导入 --> <XtxGuess ref="guessRef" /> </template> </scroll-view> </template> <style lang="scss"> // 修改小程序的颜色 类似html5中为body指定颜色 page { background-color: #f7f7f7; height: 100%; display: flex; flex-direction: column; } .scroll-view { flex: 1; } </style>
Day 3
热门推荐
注意!! 删掉hot页面的时候不会减少
import { http } from '@/utils/http' import type { PageParams } from '@/types/global' type HotParams = PageParams & { /** Tab 项的 id,默认查询全部 Tab 项的第 1 页数据 */ subType?: string } /** * 通用热门推荐类型 * @param url 请求地址 * @param data 请求参数 */ export const getHotRecommendAPI = (url: string, data?: HotParams ) => { return http({ method: 'GET', url, data, }) }
我看不懂上面这段带代码!
动态获取热门数据
热门推荐
前端类型复用思想
热门推荐页面渲染和tab交互
为什么有这么多不同的ref
- 中间那个是组型
- 为什么使用v-show 因为v-if会反复销毁创建 而v-show只是切换 滚动记录也是会独立
- 没有什么值得说的 就是基础的项目渲染啥的 唯一特殊的是介绍了一个配置开发环境
通用项目技巧
开发页面环境
商品分类
编译模式
- 这里也没有什么好说的 内容很重复 建议自己独立写一遍
商品详情
Day 4
小程序快捷登录
非空断言的正确认识
小程序页面跳转
小程序的页面跳转分为普通页面和tab页面
普通页面使用navigateTo tab页面使用switchTab
// 模拟手机号码快捷登录 const onGetphonenumberSimple = async () => { const res = await postLoginWxMinSimpleAPI('17338870680') const memberStore = useMemberStore() // pinia自带存储数据方法 memberStore.setProfile(res.result) uni.showToast({ icon: 'none', title: '登录成功' }) // 使用这段代码是因为 跳转tab页面会销毁掉之前的页面 所以会看不到提示 setTimeout(() => { uni.switchTab({ url: '/pages/my/my ' }) }, 500) }
会员中心
猜你喜欢组件封装
- 这里其实不止猜你喜欢可以封装 还有首页用到的轮播图也可以使用组件进行封装
轮播图组件封装
// 获取轮播图函数 export const useBannerList = () => { const bannerList = ref<BannerItem[]>([]) const getBannerData = async () => { const res = await getHomeBannerAPI(2) bannerList.value = res.result } return { getBannerData, bannerList } }
import { useBannerList } from '@/composables/index' const { getBannerData, bannerList } = useBannerList()
//在index.vue中需要取个别名 import { useBannerList } from '@/composables/index' const { getBannerData: getHomeBannerDate, bannerList } = useBannerList()
- 用到轮播图的地方就能使用
注意 有时候不是你的代码写的有问题 这里也会有开头提到的后端请求问题
这里如果分包没有加载成功 请检查
- 正确的是 pagesMember
- settings
- 博主眼睛不太好 在这里看了半天都没发现自己写的代码有问题
{ // pages.json // 组件自动映入规则 "easycom": { // 开始自动扫描 "autoscan": true, // 正则方式匹配 "custom": { // uni-ui 规则如下配置 "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue", // 以Xtx 开头的组件,在components文件中查找引入(需要重启服务器) "^Xtx(.*)": "@/components/Xtx$1.vue" } }, "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages // src/pages.json { "path": "pages/index/index", "style": { "navigationStyle": "custom", // 隐藏默认导航 "navigationBarTextStyle": "white", "navigationBarTitleText": "首页" } }, { "path": "pages/my/my", "style": { "navigationBarTitleText": "我的" } }, { "path": "pages/cart/cart", "style": { "navigationBarTitleText": "购物车" } }, { "path": "pages/category/category", "style": { "navigationBarTitleText": "分类" } }, { "path": "pages/login/login", "style": { "navigationStyle": "custom", // 隐藏默认导航 "navigationBarTextStyle": "white", "navigationBarTitleText": "登录" } }, { "path": "pages/hot/hot", "style": { "navigationBarTitleText": "热门推荐" } }, { "path": "pages/goods/goods", "style": { "navigationBarTitleText": "商品详情" } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "", "navigationBarBackgroundColor": "#F8F8F8", "backgroundColor": "#F8F8F8" }, // 设置 TabBar "tabBar": { "color": "#333", "selectedColor": "#27ba9b", "backgroundColor": "#fff", "borderStyle": "white", "list": [ { "text": "首页", "pagePath": "pages/index/index", "iconPath": "static/tabs/home_default.png", "selectedIconPath": "static/tabs/home_selected.png" }, { "text": "分类", "pagePath": "pages/category/category", "iconPath": "static/tabs/category_default.png", "selectedIconPath": "static/tabs/category_selected.png" }, { "text": "购物车", "pagePath": "pages/cart/cart", "iconPath": "static/tabs/cart_default.png", "selectedIconPath": "static/tabs/cart_selected.png" }, { "text": "我的", "pagePath": "pages/my/my", "iconPath": "static/tabs/user_default.png", "selectedIconPath": "static/tabs/user_selected.png" } ] }, "subPackages": [ { "root": "pagesMember", "pages": [ { "path": "settings/settings", "style": { "navigationBarTitleText": "设置" } } ] } ] }
个人信息页
个人信息展示
- 这里面有各种各样的数据渲染 不过不是很难
- 主要有判断类型、分割字符之类的
会员信息展示的琐屑代码
<script setup lang="ts"> import { getMemberProfileAPI } from '@/services/profile' import type { ProfileDetail } from '@/types/member' import { onLoad } from '@dcloudio/uni-app' import { ref } from 'vue' // 获取屏幕边界到安全区域距离 const { safeAreaInsets } = uni.getSystemInfoSync() // 获取个人信息 const profile = ref<ProfileDetail>() const getMemberProfileData = async () => { const res = await getMemberProfileAPI() profile.value = res.result } onLoad(() => { getMemberProfileData() }) </script> <template> <view class="viewport"> <!-- 导航栏 --> <view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"> <navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator> <view class="title">个人信息</view> </view> <!-- 头像 --> <view class="avatar"> <view class="avatar-content"> <image class="image" :src="profile?.avatar" mode="aspectFill" /> <text class="text">点击修改头像</text> </view> </view> <!-- 表单 --> <view class="form"> <!-- 表单内容 --> <view class="form-content"> <view class="form-item"> <text class="label">账号</text> <text class="account">{{ profile?.account }}</text> </view> <view class="form-item"> <text class="label">昵称</text> <input class="input" type="text" placeholder="请填写昵称" :value="profile?.nickname" /> </view> <view class="form-item"> <text class="label">性别</text> <radio-group> <label class="radio"> <radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" /> 男 </label> <label class="radio"> <radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" /> 女 </label> </radio-group> </view> <view class="form-item"> <text class="label">出生日期</text> <picker class="picker" mode="date" :value="profile?.birthday" start="1900-01-01" :end="new Date()" > <view v-if="profile?.birthday">{{ profile?.birthday }}</view> <view class="placeholder" v-else>请选择日期</view> </picker> </view> <view class="form-item"> <text class="label">城市</text> <picker class="picker" :value="profile?.fullLocation?.split(' ')" mode="region"> <view v-if="profile?.fullLocation">{{ profile.fullLocation }}</view> <view class="placeholder" v-else>请选择城市</view> </picker> </view> <view class="form-item"> <text class="label">职业</text> <input class="input" type="text" placeholder="请填写职业" :value="profile?.profession" /> </view> </view> <!-- 提交按钮 --> <button class="form-button">保 存</button> </view> </view> </template> <style lang="scss"> page { background-color: #f4f4f4; } .viewport { display: flex; flex-direction: column; height: 100%; background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png); background-size: auto 420rpx; background-repeat: no-repeat; } // 导航栏 .navbar { position: relative; .title { height: 40px; display: flex; justify-content: center; align-items: center; font-size: 16px; font-weight: 500; color: #fff; } .back { position: absolute; height: 40px; width: 40px; left: 0; font-size: 20px; color: #fff; display: flex; justify-content: center; align-items: center; } } // 头像 .avatar { text-align: center; width: 100%; height: 260rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; .image { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #eee; } .text { display: block; padding-top: 20rpx; line-height: 1; font-size: 26rpx; color: #fff; } } // 表单 .form { background-color: #f4f4f4; &-content { margin: 20rpx 20rpx 0; padding: 0 20rpx; border-radius: 10rpx; background-color: #fff; } &-item { display: flex; height: 96rpx; line-height: 46rpx; padding: 25rpx 10rpx; background-color: #fff; font-size: 28rpx; border-bottom: 1rpx solid #ddd; &:last-child { border: none; } .label { width: 180rpx; color: #333; } .account { color: #666; } .input { flex: 1; display: block; height: 46rpx; } .radio { margin-right: 20rpx; } .picker { flex: 1; } .placeholder { color: #808080; } } &-button { height: 80rpx; text-align: center; line-height: 80rpx; margin: 30rpx 20rpx; color: #fff; border-radius: 80rpx; font-size: 30rpx; background-color: #27ba9b; } } </style>
- 教程使用的是chooseMedia,只能在小程序中使用,多端需要考虑使用chooseimage
渲染与修改
- 这里需要对昵称数据进行即修改又使用
- v-model会对数据进行先读取再绑定 但这时候不能为空 所以读不到
const onSubmit = async () => { console.log('123') const res = await putMemberProfileAPI({ nickname: profile.value?.nickname, }) uni.showToast({ icon: 'success', title: '保存成功' }) } <!-- 提交按钮 --> <button @tap="onSubmit" class="form-button">保 存</button>
跟新store信息
// 这里将gender设置的类型不是string 所以需要使用as 指定为gender const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => { profile.value.gender = ev.detail.value as Gender }
- 记得再submit中将参数传递给后端
会员修改城市
最终代码
<script setup lang="ts"> import { getMemberProfileAPI, putMemberProfileAPI } from '@/services/profile' import { useMemberStore } from '@/stores' import type { Gender, ProfileDetail } from '@/types/member' import { onLoad } from '@dcloudio/uni-app' import { ref } from 'vue' // 获取屏幕边界到安全区域距离 const { safeAreaInsets } = uni.getSystemInfoSync() // 获取个人信息 修改个人信息所需初始值 const profile = ref({} as ProfileDetail) const getMemberProfileData = async () => { const res = await getMemberProfileAPI() profile.value = res.result } onLoad(() => { getMemberProfileData() }) const memberStore = useMemberStore() // 修改头像 const onAvatarChange = () => { // 调用拍照/选择图片 uni.chooseMedia({ // 文件个数 count: 1, // 文件类型 mediaType: ['image'], success: (res) => { // 本地路径 const { tempFilePath } = res.tempFiles[0] // 文件上传 uni.uploadFile({ url: '/member/profile/avatar', name: 'file', // 后端数据字段名 filePath: tempFilePath, // 新头像 success: (res) => { // 判断状态码是否上传成功 if (res.statusCode === 200) { // 提取头像 const { avatar } = JSON.parse(res.data).result // 当前页面更新头像 profile.value!.avatar = avatar // 更新 Store 头像 memberStore.profile!.avatar = avatar uni.showToast({ icon: 'success', title: '更新成功' }) } else { uni.showToast({ icon: 'error', title: '出现错误' }) } }, }) }, }) } // 这里将gender设置的类型不是string 所以需要使用as 指定为gender const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => { profile.value.gender = ev.detail.value as Gender } // 修改生日 const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => { profile.value.birthday = ev.detail.value } // 修改城市 let fullLocationCode: [string, string, string] = ['', '', ''] const onFullLocationChange: UniHelper.RegionPickerOnChange = (ev) => { // 修改前端界面 profile.value.fullLocation = ev.detail.value.join(' ') // 提交后端更新 fullLocationCode = ev.detail.code! } const onSubmit = async () => { const { nickname, gender, birthday, profession } = profile.value const res = await putMemberProfileAPI({ nickname, gender, birthday, profession, provinceCode: fullLocationCode[0], cityCode: fullLocationCode[1], countyCode: fullLocationCode[2], }) // 更新store昵称 memberStore.profile!.nickname = res.result.nickname uni.showToast({ icon: 'success', title: '保存成功' }) setTimeout(() => { uni.navigateBack() }, 400) } </script> <template> <view class="viewport"> <!-- 导航栏 --> <view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"> <navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator> <view class="title">个人信息</view> </view> <!-- 头像 --> <view class="avatar"> <view @tap="onAvatarChange" class="avatar-content"> <image class="image" :src="profile?.avatar" mode="aspectFill" /> <text class="text">点击修改头像</text> </view> </view> <!-- 表单 --> <view class="form"> <!-- 表单内容 --> <view class="form-content"> <view class="form-item"> <text class="label">账号</text> <text class="account">{{ profile?.account }}</text> </view> <view class="form-item"> <text class="label">昵称</text> <input class="input" type="text" placeholder="请填写昵称" v-model="profile!.nickname" /> </view> <view class="form-item"> <text class="label">性别</text> <radio-group @change="onGenderChange"> <label class="radio"> <radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" /> 男 </label> <label class="radio"> <radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" /> 女 </label> </radio-group> </view> <view class="form-item"> <text class="label">生日</text> <picker class="picker" mode="date" start="1900-01-01" :end="new Date()" :value="profile.birthday" @change="onBirthdayChange" > <view v-if="profile.birthday">{{ profile.birthday }}</view> <view class="placeholder" v-else>请选择日期</view> </picker> </view> <view class="form-item"> <text class="label">城市</text> <picker class="picker" mode="region" :value="profile.fullLocation?.split(' ')" @change="onFullLocationChange" > <view v-if="profile.fullLocation">{{ profile.fullLocation }}</view> <view class="placeholder" v-else>请选择城市</view> </picker> </view> <view class="form-item"> <text class="label">职业</text> <input class="input" type="text" placeholder="请填写职业" :value="profile?.profession" /> </view> </view> <!-- 提交按钮 --> <button @tap="onSubmit" class="form-button">保 存</button> </view> </view> </template> <style lang="scss"> page { background-color: #f4f4f4; } .viewport { display: flex; flex-direction: column; height: 100%; background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png); background-size: auto 420rpx; background-repeat: no-repeat; } // 导航栏 .navbar { position: relative; .title { height: 40px; display: flex; justify-content: center; align-items: center; font-size: 16px; font-weight: 500; color: #fff; } .back { position: absolute; height: 40px; width: 40px; left: 0; font-size: 20px; color: #fff; display: flex; justify-content: center; align-items: center; } } // 头像 .avatar { text-align: center; width: 100%; height: 260rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; .image { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #eee; } .text { display: block; padding-top: 20rpx; line-height: 1; font-size: 26rpx; color: #fff; } } // 表单 .form { background-color: #f4f4f4; &-content { margin: 20rpx 20rpx 0; padding: 0 20rpx; border-radius: 10rpx; background-color: #fff; } &-item { display: flex; height: 96rpx; line-height: 46rpx; padding: 25rpx 10rpx; background-color: #fff; font-size: 28rpx; border-bottom: 1rpx solid #ddd; &:last-child { border: none; } .label { width: 180rpx; color: #333; } .account { color: #666; } .input { flex: 1; display: block; height: 46rpx; } .radio { margin-right: 20rpx; } .picker { flex: 1; } .placeholder { color: #808080; } } &-button { height: 80rpx; text-align: center; line-height: 80rpx; margin: 30rpx 20rpx; color: #fff; border-radius: 80rpx; font-size: 30rpx; background-color: #27ba9b; } } </style>
TS开发项目于JS开发项目的不同
- 不同之处就是指定类型
- 在编写发送请求的时候,js项目只要http{}然后开始写相关接口数据而Ts项目还要给http<>{}指定一个类型,用来判断请求返回值是否符合编写时的要求
- TS的类型有别于java类型,简单的number是对应int、long。但自定义的数据类型,如果要用java来理解的话,我认为TS类型像数据结构,像枚举。 TS定义的类型就是数据的组织形式,这个组织形式是根据返回的数据不同而不同
fy-content: center; align-items: center; font-size: 16px; font-weight: 500; color: #fff; } .back { position: absolute; height: 40px; width: 40px; left: 0; font-size: 20px; color: #fff; display: flex; justify-content: center; align-items: center; } } // 头像 .avatar { text-align: center; width: 100%; height: 260rpx; display: flex; flex-direction: column; justify-content: center; align-items: center; .image { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #eee; } .text { display: block; padding-top: 20rpx; line-height: 1; font-size: 26rpx; color: #fff; } } // 表单 .form { background-color: #f4f4f4; &-content { margin: 20rpx 20rpx 0; padding: 0 20rpx; border-radius: 10rpx; background-color: #fff; } &-item { display: flex; height: 96rpx; line-height: 46rpx; padding: 25rpx 10rpx; background-color: #fff; font-size: 28rpx; border-bottom: 1rpx solid #ddd;
&:last-child { border: none; } .label { width: 180rpx; color: #333; } .account { color: #666; } .input { flex: 1; display: block; height: 46rpx; } .radio { margin-right: 20rpx; } .picker { flex: 1; } .placeholder { color: #808080; }
} &-button { height: 80rpx; text-align: center; line-height: 80rpx; margin: 30rpx 20rpx; color: #fff; border-radius: 80rpx; font-size: 30rpx; background-color: #27ba9b; } }
# TS开发项目于JS开发项目的不同 + 不同之处就是指定类型 + 在编写发送请求的时候,js项目只要http{}然后开始写相关接口数据而Ts项目还要给http<>{}指定一个类型,用来判断请求返回值是否符合编写时的要求 + TS的类型有别于java类型,简单的number是对应int、long。但自定义的数据类型,如果要用java来理解的话,我认为TS类型像数据结构,像枚举。 TS定义的类型就是数据的组织形式,这个组织形式是根据返回的数据不同而不同