一、自定义我的物品组件 my_goods.vue
<template>
<view class="goods-item">
<!-- 左侧 -->
<view class="goods-item-left">
<radio :checked="goods.goods_state" color="#c00000" v-if="showRadio" @click="radioClickhandler"></radio>
<image :src="goods.goods_small_logo || defaultPic" class="goods-pic"></image>
</view>
<!-- 右侧 -->
<view class="goods-item-right">
<!-- 商品名字 -->
<view class="goods-name">{{goods.goods_name}}</view>
<view class="goods-info-box">
<view class="good-price">¥{{goods.goods_price}}</view>
<uni-number-box :min="1" :max="9999" :value="goods.goods_count" v-if="showNum"
@change="numChangeHandler"></uni-number-box>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 商品的信息对象
goods: {
type: Object,
defaul: {},
},
showRadio: {
type: Boolean,
// 默认不展示 radio 组件
default: false
},
showNum: {
type: Boolean,
default: false
}
},
data() {
return {
// 默认的图片
defaultPic: 'https://img3.doubanio.com/f/movie/8dd0c794499fe925ae2ae89ee30cd225750457b4/pics/movie/celebrity-default-medium.png',
};
},
methods: {
// radio 组件的点击事件处理函数
radioClickhandler() {
this.$emit('radio-change', {
goods_id: this.goods.goods_id,
goods_state: !this.goods.goods_state
})
},
// 监听购物车商品数量变化的事件
numChangeHandler(val) {
this.$emit('num-change', {
goods_id: this.goods.goods_id,
goods_count: +val
})
}
}
}
</script>
<style lang="scss">
.goods-item {
display: flex;
padding: 10px 5px;
border-bottom: 1px solid #dedede;
background-color: #fff;
.goods-item-left {
margin-right: 5px;
display: flex;
justify-content: center;
align-items: center;
.goods-pic {
width: 100px;
height: 100px;
display: block;
}
}
.goods-item-right {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
.goods-name {
font-size: 13px;
}
.goods-info-box {
display: flex;
justify-content: space-between;
align-items: center;
.good-price {
color: #c00000;
font-size: 16px;
}
}
}
}
</style>
二、自定义商品列表组件 good_list.vue
<template>
<view>
<view class="goods-list">
<view v-for="(goods,i) in goodsList" :key="i" @click="gotoDetail(goods)">
<my-goods :goods="goods"></my-goods>
</view>
</view>
</view>
</template>
<script>
import {myGoods} from '@/components/my-goods/my-goods.vue'
export default {
components: {myGoods},
data() {
return {
// 请求参数对象
queryObj: {
query: '',
cid: '',
pagenum: 1,
pagesize: 10
},
goodsList: [],
total: 0,
isLoading: false
}
},
onLoad(options) {
this.queryObj.query = options.query || ''
this.queryObj.cid = options.cid || ''
this.getGoodsList()
},
methods: {
// 获取商品列表数据
async getGoodsList(cb) {
// 打开节流阀
this.isLoading = true
const {
data: res
} = await uni.$http.get('/api/public/v1/goods/search', this.queryObj)
// 关闭节流阀
this.isLoading = false
cb && cb()
if (res.meta.status !== 200) return uni.$showMsg()
this.goodsList = [...this.goodsList, ...res.message.goods]
this.total = res.message.total
},
gotoDetail(goods) {
uni.navigateTo({
url: '/subpkg/goods_detail/goods_detail?goods_id=' + goods.goods_id
})
}
},
onReachBottom() {
if (this.queryObj.pagenum * this.queryObj.pagesize >= this.total) return uni.$showMsg('数据加载完毕')
if (this.isLoading) return
// 让页码值自增+1
this.queryObj.pagenum++
this.getGoodsList()
},
onPullDownRefresh() {
// 重置关键数据
this.queryObj.pagenum = 1
this.total = 0
this.isLoading = false
this.goodsList = []
// 重新发起数据请求
this.getGoodsList(() => {
uni.stopPullDownRefresh()
})
}
}
</script>
<style lang="scss">
</style>
三、自定义商品详情组件 good_detail.vue
<template>
<view v-if="goods_info.goods_name" class="goods-detail-container">
<!-- 轮播图区域 -->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
<swiper-item v-for="(item,i) in goods_info.pics" :key="i">
<image :src="item.pics_big" @click="preview(i)"></image>
</swiper-item>
</swiper>
<!-- 商品信息区域 -->
<view class="goods-info-box">
<!-- 商品价格 -->
<view class="price">¥{{goods_info.goods_price}}</view>
<!-- 商品信息主体区域 -->
<view class="goods-info-body">
<!-- 商品名字 -->
<view class="goods-name">{{goods_info.goods_name}}</view>
<!-- 收藏 -->
<view class="favi">
<uni-icons type="star" size="18" color="gray"></uni-icons>
<text>收藏</text>
</view>
</view>
<!-- 运费 -->
<view class="yf">快递:免运费</view>
</view>
<rich-text :nodes="goods_info.goods_introduce"></rich-text>
<!-- 商品导航组件区域 -->
<view class="goods_nav">
<uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick"
@buttonClick="buttonClick" />
</view>
</view>
</template>
<script>
export default {
watch: {
total: {
handler(newVal) {
const findResult = this.options.find(x => x.text === '购物车')
if (findResult) {
findResult.info = newVal
}
},
immediate: true
}
},
data() {
return {
goods_info: {},
options: [{
icon: 'shop',
text: '店铺',
infoBackgroundColor: '#007aff',
infoColor: "red"
}, {
icon: 'cart',
text: '购物车',
info: 0
}],
buttonGroup: [{
text: '加入购物车',
backgroundColor: '#ff0000',
color: '#fff'
},
{
text: '立即购买',
backgroundColor: '#ffa200',
color: '#fff'
}
],
}
},
onLoad(options) {
const goods_id = options.goods_id
this.getGoodsDetail(goods_id)
},
methods: {
async getGoodsDetail(goods_id) {
const {
data: res
} = await uni.$http.get('/api/public/v1/goods/detail', {
goods_id
})
if (res.meta.status !== 200) return uni.$showMsg()
res.message.goods_introduce = res.message.goods_introduce.replace(//g,
')
.replace(/webp/g, 'jpg')
this.goods_info = res.message
},
preview(i) {
uni.previewImage({
current: i,
urls: this.goods_info.pics.map(x => x.pics_big)
})
},
onClick(e) {
if (e.content.text === '购物车') {
uni.switchTab({
url: '/pages/cart/cart'
})
}
},
buttonClick(e) {
if (e.content.text === '加入购物车') {
// 组织商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goods_id, goods_name, goods_price, goods_count, goods_small_logo, goods_state }
const goods = {
goods_id: this.goods_info.goods_id,
goods_name: this.goods_info.goods_name,
goods_price: this.goods_info.goods_price,
goods_count: 1,
goods_small_logo: this.goods_info.goods_small_logo,
goods_state: true
}
// 调用 addToCart 方法
// this.addToCart(goods)
}
}
}
}
</script>
<style lang="scss">
swiper {
height: 750rpx;
image {
width: 100%;
height: 100%;
}
}
.goods-info-box {
padding: 10px;
padding-right: 0;
.price {
color: #c00000;
font-size: 18px;
margin: 10px 0;
}
.goods-info-body {
display: flex;
justify-content: space-between;
.goods-name {
font-size: 13px;
margin-right: 10px;
}
.favi {
width: 120px;
font-size: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-left: 1px solid #eaeaea;
color: gray;
}
}
.yf {
font-size: 12px;
color: gray;
margin: 10px 0;
}
}
.goods_nav {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
.goods-detail-container {
padding-bottom: 50px;
}
</style>
四、自定义搜索组件 my_search.vue
<template>
<view class="my-search-container" :style="{'background-color': bgcolor}" @click="searchBoxHandler">
<view class="my-search-box" :style="{'border-radius': radius + 'px'}">
<uni-icons type="search" size="17"></uni-icons>
<text class="placeholder">搜索</text>
</view>
</view>
</template>
<script>
export default {
name: "my-search",
props: {
// 背景颜色
bgcolor: {
type: String,
default: '#c00000'
},
// 圆角尺寸
radius: {
type: Number,
default: 18 //px
}
},
data() {
return {
};
},
methods: {
searchBoxHandler() {
this.$emit('click')
}
}
}
</script>
<style lang="scss">
.my-search-container {
height: 50px;
// background-color: #c00000;
display: flex;
align-items: center;
padding: 0 10px;
.my-search-box {
width: 100%;
height: 36px;
background-color: #fff;
// border-radius: 18px;
display: flex;
justify-content: center;
align-items: center;
.placeholder {
font-size: 15px;
margin-left: 5px;
}
}
}
</style>
五、小程序首页 index.vue
<template>
<view>
<!-- 搜索组件 -->
<view class="search-box">
<my-search @click="gotoSearch"></my-search>
<!-- 动态给子组件传颜色和圆角像素值 -->
<!-- <my-search @click="gotoSearch" :bgcolor="'black'" :radius="18"></my-search> -->
</view>
<!-- 轮播图区域 -->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
<swiper-item v-for="(item,i) in swiperList" :key="i">
<navigator class="swiper-item" :url="'/subpkg/goods_detail/goods_detail?goods_id=' + item.good_id">
<image :src="item.image_src"></image>
</navigator>
</swiper-item>
</swiper>
<!-- 分类导航区域 -->
<view class="nav-list">
<view class="nav-item" v-for="(item,i) in navList" :key="i" @click="navClickHandler(item)">
<image :src="item.image_src" class="nav-img"></image>
</view>
</view>
<!-- 楼层区域 -->
<view class="floor-list">
<!-- 每个楼层的 item 项 -->
<view class="floor-item" v-for="(item,i) in floorList" :key="i">
<!-- 楼层的标题 -->
<image :src="item.floor_title.image_src" class="floor-title"></image>
<!-- 楼层的图片区域 -->
<view class="floor-img-box">
<!-- 左侧图片 -->
<navigator class="left-img-box" :url="item.product_list[0].url">
<image :src="item.product_list[0].image_src" :style="{width: item.product_list[0].image_width + 'rpx'}"
mode="widthFix">
</image>
</navigator>
<!-- 右侧图片 -->
<view class="right-img-box">
<navigator class="right-img-item" v-for="(item2,i2) in item.product_list" :key="i2" v-if="i2 !== 0"
:url="item2.url">
<image :src="item2.image_src" :style="{width: item2.image_width + 'rpx'}" mode="widthFix"></image>
</navigator>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {mySearch} from '@/components/my-search/my-search.vue'
export default {
components:{mySearch},
data() {
return {
title: 'Hello',
// 轮播图数据列表
swiperList: [],
// 分类导航的数据列表
navList: [],
// 楼层的数据
floorList: []
}
},
onLoad() {
this.getSwiperList()
this.getNavList()
this.getFloorList()
},
methods: {
gotoSearch() {
uni.navigateTo({
url: '/subpkg/search/search'
})
},
async getSwiperList() {
const {
data: res
} = await uni.$http.get('/api/public/v1/home/swiperdata')
// 请求失败
if (res.meta.status !== 200) return uni.$showMsg()
// 请求成功
this.swiperList = res.message
},
async getNavList() {
const {
data: res
} = await uni.$http.get('/api/public/v1/home/catitems')
// 请求失败
if (res.meta.status !== 200) return uni.$showMsg()
// 请求成功
this.navList = res.message
},
navClickHandler(item) {
if (item.name === '分类') {
uni.switchTab({
url: '/pages/category/category'
})
}
},
async getFloorList() {
const {
data: res
} = await uni.$http.get('/api/public/v1/home/floordata')
// 请求失败
if (res.meta.status !== 200) return uni.$showMsg()
// 请求成功
// 对每张图片的 navigator_url 数据进行处理
res.message.forEach(floor => {
floor.product_list.forEach(prod => {
prod.url = '/subpkg/goods_list/goods_list?' + prod.navigator_url.split('?')[1]
})
})
this.floorList = res.message
},
}
}
</script>
<style>
swiper {
height: 330rpx;
},
.swiper-item,
image {
width: 100%;
height: 100%;
},
.nav-list {
display: flex;
justify-content: space-around;
margin: 15px 0;
}
.nav-img {
width: 128rpx;
height: 140rpx;
}
.floor-title {
width: 100%;
height: 60rpx;
}
.floor-img-box {
display: flex;
padding-left: 10rpx;
}
.right-img-box {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.search-box {
position: sticky;
top: 0;
z-index: 999;
}
</style>
六、状态管理相关 store(以下文件分别为:store.js cart.js user.js)
import Vue from 'vue'
import Vuex from 'vuex'
import moduleCart from '@/store/cart.js'
import moduleUser from '@/store/user.js'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
'm_cart': moduleCart,
'm_user': moduleUser
}
})
export default store
export default {
namespaced: true,
state: () => ({
// 购物车的数组,用来存储购物车中每个商品的信息对象
// 每个商品的信息对象,都包含如下 6 个属性:
// { goodsId, goodsName, goodsPrice, goodsCount, goodsSmallLogo, goodsState }
cart: JSON.parse(uni.getStorageSync('cart') || '[]')
}),
mutations: {
addToCart(state, goods) {
const findResult = state.cart.find(x => x.goodsId === goods.goodsId)
if (!findResult) {
state.cart.push(goods)
} else {
findResult.goods_count++
}
this.commit('m_cart/saveToStorage')
},
saveToStorage(state) {
uni.setStorageSync('cart', JSON.stringify(state.cart))
},
// 更新购物车商品的勾选状态
updateGoodsState(state, goods) {
const findResult = state.cart.find(x => x.goodsId === goods.goodsId)
if (findResult) {
findResult.goodsState = goods.goodsState
this.commit('m_cart/saveToStorage')
}
},
updateGoodsCount(state, goods) {
const findResult = state.cart.find(x => x.goodsId === goods.goodsId)
if (findResult) {
findResult.goodsCount = goods.goodsCount
this.commit('m_cart/saveToStorage')
}
},
// 根据 id 删除对应的商品
removeGoodsById(state, goods_id) {
state.cart = state.cart.filter(x => x.goodsId !== goodsId)
this.commit('m_cart/saveToStorage')
},
// 更新购物车中所有的商品勾选状态
updateAllGoodsState(state, newState) {
state.cart.forEach(x => x.goodsState = newState)
this.commit('m_cart/saveToStorage')
}
},
getters: {
// 购物车中所有商品的总数量
total(state) {
// let c = 0
// state.cart.forEach(goods => c += goods.goods_count)
// return c
return state.cart.reduce((total, item) => total += item.goodsCount, 0)
},
// 购物车中已勾选的商品的总数量
checkedCount(state) {
return state.cart.filter(x => x.goodsState).reduce((total, item) => total += item.goodsCount, 0)
},
// 已勾选的商品的总价格
checkedGoodsAmount(state) {
return state.cart.filter(x => x.goodsState).reduce((total, item) => total += item.goodsCount * item.goodsPrice,
0).toFixed(2)
}
}
}
export default {
// 开启命名空间
namespaced: true,
// 数据
state: () => ({
address: JSON.parse(uni.getStorageSync('address') || '{}'),
token: uni.getStorageSync('token') || '',
// 用户的信息对象
userinfo: JSON.parse(uni.getStorageSync('userinfo') || '{}')
}),
mutations: {
// 更新收货地址
updateAddress(state, address) {
state.address = address
this.commit('m_user/saveAddressToStorage')
},
// 持久化存储 address
saveAddressToStorage(state) {
uni.setStorageSync('address', JSON.stringify(state.address))
},
updateUserInfo(state, userinfo) {
state.userinfo = userinfo
this.commit('m_user/saveUserInfoToStorage')
},
saveUserInfoToStorage(state) {
uni.setStorageSync('userinfo', JSON.stringify(state.userinfo))
},
updateToken(state, token) {
state.token = token
this.commit('m_user/saveTokenToStorage')
},
saveTokenToStorage(state) {
uni.setStorageSync('token', state.token)
}
},
getters: {
// 收货地址
addstr(state) {
if (!state.address.provinceName) return ''
return state.address.provinceName + state.address.cityName + state.address.countyName + state.address.detailInfo
}
}
}
七、main.js
import App from './App'
import store from '@/store/store.js'
// 导入网络请求的包
import { $http } from '@/node_modules/@escook/request-miniprogram'
uni.$http = $http
// 请求根路径
$http.baseUrl = 'https://api-hmugo-web.itheima.net'
// 请求拦截器
$http.beforeRequest = function(options) {
// 显示loading效果
uni.showLoading({
title: '数据加载中...',
})
// 判断当前请求的是否为有权限的接口
if (options.url.indexOf('/my/') !== -1) {
options.header = {
Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjIzLCJpYXQiOjE1NjQ3MzAwNzksImV4cCI6MTAwMTU2NDczMDA3OH0.YPt-XeLnjV-_1ITaXGY2FhxmCe4NvXuRnRB8OMCfnPo"
}
}
}
// 响应拦截器
$http.afterRequest = function() {
// 隐藏loading效果
uni.hideLoading()
}
// 封装数据请求失败的弹框方法
uni.$showMsg = function(title = '数据请求失败', duration = 1500) {
uni.showToast({
title,
duration,
icon: 'none',
})
}
Vue.config.productionTip = false
import uView from '@/uni_modules/uview-ui'
Vue.use(uView)
// #ifndef VUE3
import Vue from 'vue'
Vue.config.productionTip = false
App.mpType = 'app'
try {
function isPromise(obj) {
return (
!!obj &&
(typeof obj === "object" || typeof obj === "function") &&
typeof obj.then === "function"
);
}
// 统一 vue2 API Promise 化返回格式与 vue3 保持一致
uni.addInterceptor({
returnValue(res) {
if (!isPromise(res)) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (res[0]) {
reject(res[0]);
} else {
resolve(res[1]);
}
});
});
},
});
} catch (error) { }
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif
八、引入/uview组件(分别该四个文件:app.vue man.js page.json uni.scss)
<style lang="scss">
/*每个页面公共css */
@import "@/uni_modules/uview-ui/index.scss";
</style>
import uView from '@/uni_modules/uview-ui'
Vue.use(uView)
"easycom": {
"^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue"
},
@import '@/uni_modules/uview-ui/theme.scss';
九、uniapp小程序分包
①page.json中定义分包文件
②定义分包文件夹并创建分包文件
"subPackages": [
{
"root": "subpkg",
"pages": [
{
"path": "search/search"
},{
"path" : "goods_list/goods_list"
}
,{
"path" : "goods_detail/goods_detail",
"style" :
{
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
}
]
}
],