【UniApp开发小程序】小程序首页完善(滑到底部数据翻页、回到顶端、基于回溯算法的两列数据高宽比平衡)【后端基于若依管理系统开发】
文章目录
- 说明
- 细节一:首页滑动到底部,需要查询下一页的商品
- 界面预览
- 页面实现
- 细节二:当页面滑动到下方,出现一个回到顶端的悬浮按钮
- 细节三:商品分列
- 说明
- 优化前后效果对比
- 使用回溯算法实现
- Controller
- Service
- 回溯算法
- 优化:减少图片的网络请求
- 数据表增加字段
- 将数据表中已有数据的宽高比计算出来,并更新到数据表中
- 修改商品发布页面的代码
- Service改进
- 优化:考虑分页的分组高宽比总和平衡
- 页面代码
- Controller
- Service
- 回溯算法
- 页面整体代码
说明
之前已经在【UniApp开发小程序】小程序首页(展示商品、商品搜索、商品分类搜索)【后端基于若依管理系统开发】这篇文章中介绍了首页的实现,但是当时的实现并不是完善的,因为项目开发是一个持续的过程,也因为我是个人的第一次尝试开发这种类型的项目,很多细节没有提前构思清楚,因此这篇文章作为一个补充,用来优化前面的一些问题
细节一:首页滑动到底部,需要查询下一页的商品
界面预览
当滑动底部的时候,底部出现”正在加载“字样,同时向后端发送请求获取下一页的商品数据

当商品被全部加载出之后,显示“没有更多了”字样

页面实现
下面的方法可以监听用户滑动页面到达底部,当滑动到底部的时候,调用方法查询更多商品
// 监听用户滑动到底部
onReachBottom() {this.getMoreProductVo();
},
注意,当还有商品没有被查询出来时,才会调用listProductVo方法去找服务端查询数据。如果没有了,则提示“没有更多了”
/*** 获取下一页的商品*/
getMoreProductVo() {if (this.productList[0].length + this.productList[1].length >= this.total) {// this.$refs.uToast.show({// type: 'warning',// message: "已经加载完所有商品数据",// duration: 1000// })} else {if (this.loadData != true) {// console.log("--------------------------获取下一页商品---------------------------")this.page.pageNum++;// 显示正在加载this.loadmoreStatus = "loading";this.listProductVo().then(() => {if (this.productList[0].length + this.productList[1].length >= this.total) {// 没有更多了this.loadmoreStatus = "nomore";} else {// 加载更多this.loadmoreStatus = "loadmore";}});}}
},
细节二:当页面滑动到下方,出现一个回到顶端的悬浮按钮
增加一个标签
<!-- 回到上方按钮 -->
<u-back-top :scroll-top="scrollTop"></u-back-top>
因为标签绑定了一个变量,需要声明出来
// 用来控制滚动到最上方
scrollTop: 0
除此之外,还需要实时记录滚动的位置
// 在滑动过程实时获取现在的滚动条位置,并保存当前的滚动条位置
onPageScroll(e) {this.scrollTop = e.scrollTop;
},
细节三:商品分列
说明
上篇文章中,使用了最简单的方式来实现分列,那就是直接遍历一遍商品数组,依次将商品分到第一列和第二列,但是这样会出现两列商品高度不平衡的情况,如下图

因此,我们需要更换一种分组策略,用来平衡两列商品内容的高度,这样视觉效果更好。问题可以理解为:假设有十个物品,每个物品的长度不太一样,要求将这些物品分到两组中,最后两组物品长度总和最接近,请问需要怎么来分这两个组?
优化前后效果对比

使用回溯算法实现
因为采用的是分页查询,而且每次查询出来的数据量并不大,因此可以直接使用回溯算法获取所有的分组情况,最后选择出高度差距最小的分组方案即可
Controller
/*** 查询商品Vo列表*/
@PreAuthorize("@ss.hasPermi('market:product:list')")
@PostMapping("/listProductVo")
@ApiOperation("获取商品列表")
public AjaxResult listProductVo(@RequestBody ProductVo productVo) {startPage();if (productVo.getProductCategoryId() != null) {// --if-- 当分类不为空的时候,只按照分类来搜索productVo.setKeyword(null);}if (productVo.getIsSearchStar() != null && productVo.getIsSearchStar() == true) {productVo.setStarPeopleId(getLoginUser().getUserId());}List<ProductVo> productVoList = productService.selectProductVoList(productVo);// 将productVoList分成两组,要求两组的高度之和相差最小List<ProductVo>[] groups = productService.splitToTwoGroups(productVoList);Map<String, Object> map = new HashMap<>();TableDataInfo pageMes = getDataTable(productVoList);map.put("pageMes", pageMes);map.put("groups", groups);return AjaxResult.success(map);
}
Service
@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList) {List<ProductVo>[] resultArr = new List[2];for (int i = 0; i < resultArr.length; i++) {resultArr[i] = new ArrayList<>();}/// 数据准备// 获取每个图片的高宽比Map<Long, Double> idAndRatioMap = new HashMap<>();// 存储所有商品的idList<Long> idList = new ArrayList<>();long start = System.currentTimeMillis();for (ProductVo productVo : productVoList) {idList.add(productVo.getId());if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {try {BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());} catch (IOException e) {throw new RuntimeException(e);}} else {idAndRatioMap.put(productVo.getId(), 0.0);}}System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");/// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案GroupDivide groupDivide = new GroupDivide();groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap);/// 最后处理分组List<Long> group1 = groupDivide.bestGroup1;List<Long> group2 = new ArrayList<>();for (Long id : idList) {if (group1.indexOf(id) == -1) {group2.add(id);}}for (ProductVo productVo : productVoList) {if (group1.indexOf(productVo.getId()) != -1) {resultArr[0].add(productVo);} else {resultArr[1].add(productVo);}}return resultArr;
}
由于下面的方法获取每个图片的高宽比都需要进行网络请求,因此速度较慢,因此需要进行优化
BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());

回溯算法
为了加速算法的求解,其中使用了减枝策略,不去搜索没有必要搜索的方案
package com.shm.algorithm;import com.ruoyi.common.utils.clone.CloneUtil;import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** 首页商品数据分组** @Author dam* @create 2023/8/30 14:12*/
public class GroupDivide {/*** 最小间距*/private double minOffSet = Double.MAX_VALUE;/*** 存储最好的第一组*/public List<Long> bestGroup1=null;public void dfsSearch(List<Long> idList, int begin, List<Long> curGroup1, Map<Long, Double> idAndRatioMap) {if (begin == idList.size()) {// 递归完成return;}for (int i = begin; i < idList.size(); i++) {curGroup1.add(idList.get(i));// 计算组1的长度-组2的长度double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap);if (offSet > minOffSet) {// 如果当前差距已经大于最小差距,执行剪枝,因为如果再往第一组增加图片的话,那差距只会更大,没必要再往下搜索了// 删除最后一个元素curGroup1.remove(curGroup1.size() - 1);continue;} else if (Math.abs(offSet) < minOffSet) {// 找到更小的间距,保存最优解minOffSet = Math.abs(offSet);bestGroup1 = CloneUtil.arrayListClone(curGroup1);}dfsSearch(idList, i + 1, curGroup1, idAndRatioMap);// 删除最后一个元素curGroup1.remove(curGroup1.size() - 1);}}/*** 计算第一组的图片的总高度 减去 第二组图片的总高度** @param idList* @param group1* @param idAndRatioMap* @return*/private double calculateGroup1DifHeifGroup2Hei(List<Long> idList, List<Long> group1, Map<Long, Double> idAndRatioMap) {double sum1 = 0, sum2 = 0;for (Long id : idList) {if (group1.indexOf(id) == -1) {sum2 += idAndRatioMap.get(id);}else {sum1 += idAndRatioMap.get(id);}}return sum1 - sum2;}}
优化:减少图片的网络请求
因为图片的高宽比是一个不变量,可以将其作为一个属性存储到数据表中,这样只需要查询出来即可,不再需要使用网络请求来获取,但是需要在存储图片到数据表之前获取高宽比,并将该属性进行存储
数据表增加字段

将数据表中已有数据的宽高比计算出来,并更新到数据表中
因为我的数据表中已经存在了一些图片数据,为了小程序地正确运行,需要对这批数据进行修复,即为每张图片补充高宽比。因为数据表的数据量不大,而且是一次性任务,直接每次修改单条数据即可。如果数据量很大,可以使用多线程和分批批量修改来优化修复速度
@Overridepublic void updatePictureSheetSetAspectRatio() {Picture picture = new Picture();picture.setType(0);// 取消使用分页clearPage();List<Picture> pictureList = pictureMapper.selectPictureList(picture);for (Picture picture1 : pictureList) {String address = picture1.getAddress();try {BufferedImage sourceImg = ImageIO.read(new URL(address));picture1.setAspectRatio(sourceImg.getHeight() * 1.0 / sourceImg.getWidth());pictureMapper.updatePicture(picture1);} catch (IOException e) {throw new RuntimeException(e);}}}
修改商品发布页面的代码
现在数据表需要保存图片的高宽比,虽然可以直接由服务端在保存图片之前计算高宽比,但是这样还是要发送很多网络请求,影响接口的并发性能,因此建议由客户端来计算高宽比,然后直接上传给服务端,服务端直接将数据保存即可
/**
* 上传闲置商品
*/
uploadSellProduct() {// console.log("上传闲置商品picList:" + JSON.stringify(this.picList));if (this.product.productCategoryId) {if (this.picList.length == 0) {this.$refs.uToast.show({type: 'error',message: "商品图片没有上传成功"})} else {this.setPicAspectRatio().then(() => {// console.log("即将上传的商品:" + JSON.stringify(this.product));uploadSellProduct(this.product).then(res => {this.$refs.uToast.show({type: 'success',message: "您的商品已经发布到平台"})setTimeout(() => {uni.reLaunch({url: "/pages/index/index"})}, 1000)}).catch(error => {console.log("error:" + JSON.stringify(error));this.$refs.uToast.show({type: 'error',message: "商品发布失败"})});});}} else {this.$refs.uToast.show({type: 'error',message: "请选择分类"})}
},
/*** 设置图片的宽高比*/
setPicAspectRatio() {return new Promise((resolve, reject) => {this.product.picList = [];let promises = [];for (let i = 0; i < this.picList.length; i++) {let picUrl = this.picList[i];promises.push(this.getAspectRatio(picUrl).then((res) => {let pic = {address: picUrl,aspectRatio: res}this.product.picList.push(pic);console.log("当前图片高宽比设置完成");}))}Promise.all(promises).then(() => {console.log("所有图片高宽比设置完成,this.product.picList:" + JSON.stringify(this.product.picList));resolve();})})
},
/*** 获取单个图片的高宽比* @param {Object} url*/
getAspectRatio(url) {return new Promise((resolve, reject) => {uni.getImageInfo({src: url,success: function(res) {let aspectRatio = res.height / res.width;resolve(aspectRatio);}});})
},
注意点:
- 因为getAspectRatio方法获取图片的高宽比发送网络请求,因此使用Promise来确保高宽比获取成功才resolve
- 在上传商品之前,需要先设置商品所对应的所有图片的高宽比。如果图片有多张,需要等待所有图片的高宽比都设置完成,本文使用Promise.all(promises)来等待所有图片的高宽比都设置完成,再resolve
Service改进
因为已经将图片的高宽比存储到数据表中,因此不需要再发送网路请求,直接获取属性值即可
@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList) {List<ProductVo>[] resultArr = new List[2];for (int i = 0; i < resultArr.length; i++) {resultArr[i] = new ArrayList<>();}/// 数据准备// 获取每个图片的高宽比Map<Long, Double> idAndRatioMap = new HashMap<>();// 存储所有商品的idList<Long> idList = new ArrayList<>();long start = System.currentTimeMillis();for (ProductVo productVo : productVoList) {idList.add(productVo.getId());if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
// try {
// BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
// idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
// } catch (IOException e) {
// throw new RuntimeException(e);
// }idAndRatioMap.put(productVo.getId(), productVo.getPicList().get(0).getAspectRatio());} else {idAndRatioMap.put(productVo.getId(), 0.0);}}System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");/// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案GroupDivide groupDivide = new GroupDivide();groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap);/// 最后处理分组List<Long> group1 = groupDivide.bestGroup1;List<Long> group2 = new ArrayList<>();for (Long id : idList) {if (group1.indexOf(id) == -1) {group2.add(id);}}for (ProductVo productVo : productVoList) {if (group1.indexOf(productVo.getId()) != -1) {resultArr[0].add(productVo);} else {resultArr[1].add(productVo);}}return resultArr;
}
【测试】
在不需要发送网络请求之后,可以看到获取图片高宽比的时间被大大减少

优化:考虑分页的分组高宽比总和平衡
虽然上面已经使用算法来平衡两列的高宽比总和了,但是还存在一个问题,即商品数据是分页查询的,比如第第一页查询的结果是第一列的高宽比总和大于第二列的高宽比总和。那么为了可以更好地平衡两列的高宽比总和,第二页数据的查询结果应该是第二列的高宽比总和大于第一列的高宽比总和。为了处理这个问题,在使用回溯算法的时候,需要接收当前已渲染页面的两列宽高比,这样才能方便更好地进行决策
页面代码
从下面的代码中,可以很直观地看到,每次分页查询都更新两列对应地高宽比总和,并在发送请求的时候带上这两个参数
/**
* 查询商品vo集合
*/
listProductVo() {
return new Promise((resolve, reject) => {// 设置当前两列的高宽比总和this.searchForm.sumAspectRatioOfColumn1 = this.sumAspectRatioOfColumn1;this.searchForm.sumAspectRatioOfColumn2 = this.sumAspectRatioOfColumn2;listProductVo(this.searchForm, this.page).then(res => {// console.log("listProductVo:" + JSON.stringify(res))let productVoList = res.data.pageMes.rows;this.total = res.data.pageMes.total;// this.productList = [// [],// []// ];// for (var i = 0; i < productVoList.length; i++) {// if (i % 2 == 0) {// // 第一列数据// this.productList[0].push(productVoList[i]);// } else {// // 第二列数据// this.productList[1].push(productVoList[i]);// }// }let groups = res.data.groups;for (var i = 0; i < groups[0].length; i++) {if (groups[0][i].picList != null && groups[0][i].picList.length > 0) {this.sumAspectRatioOfColumn1 += groups[0][i].picList[0].aspectRatio;}}for (var i = 0; i < groups[1].length; i++) {if (groups[1][i].picList != null && groups[1][i].picList.length > 0) {this.sumAspectRatioOfColumn2 += groups[1][i].picList[0].aspectRatio;}}this.productList[0] = this.productList[0].concat(groups[0]);this.productList[1] = this.productList[1].concat(groups[1]);resolve();})})},
Controller
/*** 查询商品Vo列表*/
@PreAuthorize("@ss.hasPermi('market:product:list')")
@PostMapping("/listProductVo")
@ApiOperation("获取商品列表")
public AjaxResult listProductVo(@RequestBody ProductVo productVo) {startPage();if (productVo.getProductCategoryId() != null) {// --if-- 当分类不为空的时候,只按照分类来搜索productVo.setKeyword(null);}if (productVo.getIsSearchStar() != null && productVo.getIsSearchStar() == true) {productVo.setStarPeopleId(getLoginUser().getUserId());}List<ProductVo> productVoList = productService.selectProductVoList(productVo);// 将productVoList分成两组,要求两组的高度之和相差最小List<ProductVo>[] groups = productService.splitToTwoGroups(productVoList, productVo.getSumAspectRatioOfColumn1(), productVo.getSumAspectRatioOfColumn2());Map<String, Object> map = new HashMap<>();TableDataInfo pageMes = getDataTable(productVoList);map.put("pageMes", pageMes);map.put("groups", groups);return AjaxResult.success(map);
}
Service
@Override
public List<ProductVo>[] splitToTwoGroups(List<ProductVo> productVoList, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {List<ProductVo>[] resultArr = new List[2];for (int i = 0; i < resultArr.length; i++) {resultArr[i] = new ArrayList<>();}/// 数据准备// 获取每个图片的高宽比Map<Long, Double> idAndRatioMap = new HashMap<>();// 存储所有商品的idList<Long> idList = new ArrayList<>();long start = System.currentTimeMillis();for (ProductVo productVo : productVoList) {idList.add(productVo.getId());if (productVo.getPicList() != null && productVo.getPicList().size() > 0) {
// try {
// BufferedImage sourceImg = ImageIO.read(new URL(productVo.getPicList().get(0)).openStream());
// idAndRatioMap.put(productVo.getId(), sourceImg.getHeight() * 1.0 / sourceImg.getWidth());
// } catch (IOException e) {
// throw new RuntimeException(e);
// }idAndRatioMap.put(productVo.getId(), productVo.getPicList().get(0).getAspectRatio());} else {idAndRatioMap.put(productVo.getId(), 0.0);}}System.out.println("分组时间:" + (System.currentTimeMillis() - start) + "ms");/// 深度优先遍历,找出所有方案,选择两组高度差距最小的分组方案GroupDivide groupDivide = new GroupDivide();groupDivide.dfsSearch(idList, 0, new ArrayList<>(), idAndRatioMap,sumAspectRatioOfColumn1,sumAspectRatioOfColumn2);/// 最后处理分组List<Long> group1 = groupDivide.bestGroup1;List<Long> group2 = new ArrayList<>();for (Long id : idList) {if (group1.indexOf(id) == -1) {group2.add(id);}}for (ProductVo productVo : productVoList) {if (group1.indexOf(productVo.getId()) != -1) {resultArr[0].add(productVo);} else {resultArr[1].add(productVo);}}return resultArr;
}
回溯算法
package com.shm.algorithm;import com.ruoyi.common.utils.clone.CloneUtil;import java.util.List;
import java.util.Map;/*** 首页商品数据分组** @Author dam* @create 2023/8/30 14:12*/
public class GroupDivide {/*** 最小间距*/private double minOffSet = Double.MAX_VALUE;/*** 存储最好的第一组*/public List<Long> bestGroup1 = null;public void dfsSearch(List<Long> idList, int begin, List<Long> curGroup1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {if (begin == idList.size()) {// 递归完成return;}for (int i = begin; i < idList.size(); i++) {curGroup1.add(idList.get(i));// 计算组1的长度-组2的长度double offSet = calculateGroup1DifHeifGroup2Hei(idList, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2);if (offSet > minOffSet) {// 如果当前差距已经大于最小差距,执行剪枝,因为如果再往第一组增加图片的话,那差距只会更大,没必要再往下搜索了// 删除最后一个元素curGroup1.remove(curGroup1.size() - 1);continue;} else if (Math.abs(offSet) < minOffSet) {// 找到更小的间距,保存最优解minOffSet = Math.abs(offSet);bestGroup1 = CloneUtil.arrayListClone(curGroup1);}dfsSearch(idList, i + 1, curGroup1, idAndRatioMap, sumAspectRatioOfColumn1, sumAspectRatioOfColumn2);// 删除最后一个元素curGroup1.remove(curGroup1.size() - 1);}}/*** 计算第一组的图片的总高度 减去 第二组图片的总高度** @param idList* @param group1* @param idAndRatioMap* @param sumAspectRatioOfColumn1* @param sumAspectRatioOfColumn2* @return*/private double calculateGroup1DifHeifGroup2Hei(List<Long> idList, List<Long> group1, Map<Long, Double> idAndRatioMap, Double sumAspectRatioOfColumn1, Double sumAspectRatioOfColumn2) {// 设置初始值double sum1 = sumAspectRatioOfColumn1, sum2 = sumAspectRatioOfColumn2;for (Long id : idList) {if (group1.indexOf(id) == -1) {sum2 += idAndRatioMap.get(id);} else {sum1 += idAndRatioMap.get(id);}}return sum1 - sum2;}}
页面整体代码
【index页面】
<template><view class="content"><u-toast ref="uToast"></u-toast><!-- 回到上方按钮 --><u-back-top :scroll-top="scrollTop"></u-back-top><view style="display: flex;align-items: center;"><u-search placeholder="请输入商品名称" v-model="searchForm.keyword" @search="listProductVo" :showAction="false":clearabled="true"></u-search><text class="iconfont" style="font-size: 35px;" @click="selectCategory()"></text></view><u-row customStyle="margin-top: 10px" gutter="10" align="start" v-if="productList[0].length>0&&loadData==false"><u-col span="6" class="col" v-for="(data,index) in productList" :key="index"><view class="productVoItem" v-for="(productVo,index1) in data" :key="index1"@click="seeProductDetail(productVo)"><u--image v-if="productVo.picList!=null&&productVo.picList.length>0" :showLoading="true":src="productVo.picList[0].address" width="100%":height="productVo.picList[0].aspectRatio*100+'%'" radius="10" mode="widthFix" :lazy-load="true":fade="true" duration="450" @error="reloadPir(productVo.picList[0].address)"><!-- 加载失败展示 --><view slot="error" style="font-size: 24rpx;">加载失败</view><!-- 加载中提示 --><template v-slot:loading><u-loading-icon color="red"></u-loading-icon></template></u--image><!-- <u--image v-else :showLoading="true" :src="src" @click="click"></u--image> --><view class="productMes"><text class="productName">【{{productVo.name}}】</text><text>{{productVo.description==null?'':productVo.description}}</text></view><view style="display: flex;align-items: center;"><!-- 现价 --><view class="price">¥<text class="number">{{productVo.price}}</text>/{{productVo.unit}}</view><view style="width: 10px;"></view><!-- 原价 --><view class="originPrice">¥{{productVo.originalPrice}}/{{productVo.unit}}</view></view><view style="display: flex;align-items: center;"><u--image :src="productVo.avatar" width="20" height="20" shape="circle"></u--image><view style="width: 10px;"></view><view> {{productVo.nickname}}</view></view></view></u-col></u-row><!-- 显示加载相关字样 --><u-loadmore v-if="productList[0].length>0&&loadData==false" :status="loadmoreStatus" /><u-empty v-if="productList[0].length==0&&loadData==false" mode="data" texColor="#ffffff" iconSize="180"iconColor="#D7DEEB" text="所选择的分类没有对应的商品,请重新选择" textColor="#D7DEEB" textSize="18" marginTop="30"></u-empty><view style="margin-top: 20px;" v-if="loadData==true"><u-skeleton :loading="true" :animate="true" rows="10"></u-skeleton></view><!-- 浮动按钮 --><FloatButton @click="cellMyProduct()"><u--image :src="floatButtonPic" shape="circle" width="60px" height="60px"></u--image></FloatButton></view>
</template><script>import FloatButton from "@/components/FloatButton/FloatButton.vue";import {listProductVo} from "@/api/market/product.js";import pictureApi from "@/utils/picture.js";import Vue from 'vue';import {debounce} from "@/utils/debounce.js"export default {components: {FloatButton},onShow: function() {let categoryNameList = uni.getStorageSync("categoryNameList");if (categoryNameList) {this.categoryNameList = categoryNameList;this.searchForm.productCategoryId = uni.getStorageSync("productCategoryId");this.searchForm.keyword = this.getCategoryLayerName(this.categoryNameList);uni.removeStorageSync("categoryNameList");uni.removeStorageSync("productCategoryId");this.listProductVo();}},data() {return {title: 'Hello',// 浮动按钮的图片floatButtonPic: require("@/static/cellLeaveUnused.png"),searchForm: {// 商品搜索关键词keyword: "",productCategoryId: undefined},productList: [[],[]],loadData: false,// 用来锁定,防止多次同时进行websocket连接lockReconnect: false,// 心跳一次间隔的时间,单位毫秒heartbeatTime: 5000,page: {pageNum: 1,pageSize: 10},// 总数据条数total: 0,// 数据加载状态loadmoreStatus: "loadmore",// 用来控制滚动到最上方scrollTop: 0,// 分别存储两列的高宽比总和sumAspectRatioOfColumn1: 0,sumAspectRatioOfColumn2: 0,}},onLoad() {},created() {this.loadData = true;this.listProductVo().then(() => {this.loadData = false;});this.initWebsocket();// this.getMoreProductVo = debounce(this.getMoreProductVo);},// 监听用户滑动到底部onReachBottom() {this.getMoreProductVo();},// 在滑动过程实时获取现在的滚动条位置,并保存当前的滚动条位置onPageScroll(e) {this.scrollTop = e.scrollTop;},methods: {/*** 查询商品vo集合*/listProductVo() {return new Promise((resolve, reject) => {// 设置当前两列的高宽比总和this.searchForm.sumAspectRatioOfColumn1 = this.sumAspectRatioOfColumn1;this.searchForm.sumAspectRatioOfColumn2 = this.sumAspectRatioOfColumn2;listProductVo(this.searchForm, this.page).then(res => {// console.log("listProductVo:" + JSON.stringify(res))let productVoList = res.data.pageMes.rows;this.total = res.data.pageMes.total;// this.productList = [// [],// []// ];// for (var i = 0; i < productVoList.length; i++) {// if (i % 2 == 0) {// // 第一列数据// this.productList[0].push(productVoList[i]);// } else {// // 第二列数据// this.productList[1].push(productVoList[i]);// }// }let groups = res.data.groups;for (var i = 0; i < groups[0].length; i++) {if (groups[0][i].picList != null && groups[0][i].picList.length > 0) {this.sumAspectRatioOfColumn1 += groups[0][i].picList[0].aspectRatio;}}for (var i = 0; i < groups[1].length; i++) {if (groups[1][i].picList != null && groups[1][i].picList.length > 0) {this.sumAspectRatioOfColumn2 += groups[1][i].picList[0].aspectRatio;}}this.productList[0] = this.productList[0].concat(groups[0]);this.productList[1] = this.productList[1].concat(groups[1]);resolve();})})},/*** 获取下一页的商品*/getMoreProductVo() {if (this.productList[0].length + this.productList[1].length >= this.total) {// this.$refs.uToast.show({// type: 'warning',// message: "已经加载完所有商品数据",// duration: 1000// })} else {if (this.loadData != true) {// console.log("--------------------------获取下一页商品---------------------------")this.page.pageNum++;// 显示正在加载this.loadmoreStatus = "loading";this.listProductVo().then(() => {if (this.productList[0].length + this.productList[1].length >= this.total) {// 没有更多了this.loadmoreStatus = "nomore";} else {// 加载更多this.loadmoreStatus = "loadmore";}});}}},/*** 跳转到卖闲置页面*/cellMyProduct() {console.log("我要卖闲置");uni.navigateTo({url: "/pages/sellMyProduct/sellMyProduct"})},/*** 获取高宽比 乘以 100%*/getAspectRatio(url) {return pictureApi.getAspectRatio(url);},/*** 选择分类*/selectCategory() {uni.navigateTo({url: "/pages/sellMyProduct/selectCategory"})},/*** 获取商品名称*/getCategoryLayerName() {let str = '';for (let i = 0; i < this.categoryNameList.length - 1; i++) {str += this.categoryNameList[i] + '/';}return str + this.categoryNameList[this.categoryNameList.length - 1];},/*** 查看商品的详情*/seeProductDetail(productVo) {// console.log("productVo:"+JSON.stringify(productVo))uni.navigateTo({url: "/pages/product/detail?productVo=" + encodeURIComponent(JSON.stringify(productVo))})},/*** 重新加载图片*/reloadPir(pic) {console.log("图片加载失败,pic:" + pic)},/*** 创建websocket连接*/initWebsocket() {console.log("this.socket:" + JSON.stringify(this.$socket))// this.$socket == null,刚刚进入首页,还没有建立过websocket连接// this.$socket.readyState==0 表示正在连接当中// this.$socket.readyState==1 表示处于连接状态// this.$socket.readyState==2 表示连接正在关闭// this.$socket.readyState==3 表示连接已经关闭if (this.$socket == null || (this.$socket.readyState != 1 && this.$socket.readyState != 0)) {this.$socket = uni.connectSocket({url: "ws://10.23.17.146:8085/websocket/" + uni.getStorageSync("curUser").userName,success(res) {console.log('WebSocket连接成功', res);},})// console.log("this.socket:" + this.$socket)// 监听WebSocket连接打开事件this.$socket.onOpen((res) => {console.log("websocket连接成功")Vue.prototype.$socket = this.$socket;// 连接成功,开启心跳this.headbeat();});// 连接异常this.$socket.onError((res) => {console.log("websocket连接出现异常");// 重连this.reconnect();})// 连接断开this.$socket.onClose((res) => {console.log("websocket连接关闭");// 重连this.reconnect();})}},/*** 重新连接*/reconnect() {// console.log("重连");// 防止重复连接if (this.lockReconnect == true) {return;}// 锁定,防止重复连接this.lockReconnect = true;// 间隔一秒再重连,避免后台服务出错时,客户端连接太频繁setTimeout(() => {this.initWebsocket();}, 1000)// 连接完成,设置为falsethis.lockReconnect = false;},// 开启心跳headbeat() {// console.log("websocket心跳");var that = this;setTimeout(function() {if (that.$socket.readyState == 1) {// websocket已经连接成功that.$socket.send({data: JSON.stringify({status: "ping"})})// 调用启动下一轮的心跳that.headbeat();} else {// websocket还没有连接成功,重连that.reconnect();}}, that.heartbeatTime);},}}
</script><style lang="scss">.content {padding: 20rpx;.col {width: 50%;}.productVoItem {margin-bottom: 20px;.productMes {overflow: hidden;text-overflow: ellipsis;display: -webkit-box;/* 显示2行 */-webkit-line-clamp: 2;-webkit-box-orient: vertical;.productName {font-weight: bold;}}.price {color: #F84442;font-weight: bold;.number {font-size: 22px;}}.originPrice {color: #A2A2A2;font-size: 15px;// 给文字添加中划线text-decoration: line-through;}}}
</style>
【上传销售商品页面】
<template><view class="container"><u-toast ref="uToast"></u-toast><view class="content"><view class="item"><view class="labelName">商品名称</view><u--input placeholder="请输入商品名称" border="surround" v-model="product.name"></u--input></view><u-divider text="商品描述和外观"></u-divider><!-- 商品描述 --><u--textarea v-model="product.description" placeholder="请输入商品描述" height="150"></u--textarea><!-- 图片上传 --><view><imageUpload v-model="picList" maxCount="9"></imageUpload></view><u-divider text="分类选择/自定义标签"></u-divider><!-- 分类选择/自定义标签 --><view class="item"><view class="labelName">分类</view><view class="selectTextClass" @click="selectCategory">{{getCategoryLayerName()}}</view></view><!-- 商品的属性 新度 功能完整性 --><view class="item"><view class="labelName">成色</view><view class="columnClass"><view :class="product.fineness==index?'selectTextClass':'textClass'"v-for="(finessName,index) in finenessList" :key="index" @click="changeFineness(index)">{{finessName}}</view></view></view><view class="item"><view class="labelName">功能状态</view><view class="columnClass"><view :class="product.functionalStatus==index?'selectTextClass':'textClass'"v-for="(functionName,index) in functionList" :key="index"@click="changeFunctionalStatus(index)">{{functionName}}</view></view></view><u-row customStyle="margin-bottom: 10px"><u-col span="5"><view class="item"><view class="labelName">数量</view><u--input placeholder="请输入商品数量" border="surround" v-model="product.number"></u--input></view></u-col><u-col span="7"><view class="item"><view class="labelName">计量单位</view><u--input placeholder="请输入计量单位" border="surround" v-model="product.unit"></u--input></view></u-col></u-row><!-- 价格 原价 现价 --><u-divider text="价格"></u-divider><u-row customStyle="margin-bottom: 10px"><u-col span="6"><view class="item"><view class="labelName">原价</view><u-input placeholder="请输入原价" border="surround" v-model="product.originalPrice" color="#ff0000"@blur="originalPriceChange"><u--text text="¥" slot="prefix" margin="0 3px 0 0" type="error"></u--text></u-input></view></u-col><u-col span="6"><view class="item"><view class="labelName">出售价格</view><u-input placeholder="请输入出售价格" border="surround" v-model="product.price" color="#ff0000"@blur="priceChange"><u--text text="¥" slot="prefix" margin="0 3px 0 0" type="error"></u--text></u-input></view></u-col></u-row><u-button text="出售" size="large" type="primary" @click="uploadSellProduct"></u-button></view></view>
</template><script>import imageUpload from "@/components/ImageUpload/ImageUpload.vue";import {uploadSellProduct} from "@/api/market/product.js"export default {components: {imageUpload},onShow: function() {let categoryNameList = uni.getStorageSync("categoryNameList");if (categoryNameList) {this.categoryNameList = categoryNameList;this.product.productCategoryId = uni.getStorageSync("productCategoryId");uni.removeStorageSync("categoryNameList");uni.removeStorageSync("productCategoryId");}},data() {return {product: {name: '',descripption: '',picList: [],productCategoryId: undefined,number: 1,unit: '个',isContribute: 0,originalPrice: 0.00,price: 0.00,// 成色fineness: 0,// 功能状态functionalStatus: 0,brandId: 0},value: 'dasdas',categoryNameList: ["选择分类"],finenessList: ["全新", "几乎全新", "轻微使用痕迹", "明显使用痕迹", "外观破损"],functionList: ["功能完好无维修", "维修过,可正常使用", "有小问题,不影响使用", "无法正常使用"],picList: [],}},methods: {getCategoryLayerName() {let str = '';for (let i = 0; i < this.categoryNameList.length - 1; i++) {str += this.categoryNameList[i] + '/';}return str + this.categoryNameList[this.categoryNameList.length - 1];},/*** 价格校验* @param {Object} price 价格*/priceVerify(price) {if (isNaN(price)) {this.$refs.uToast.show({type: 'error',message: "输入的价格不是数字,请重新输入"})return false;}if (price < 0) {this.$refs.uToast.show({type: 'error',message: "输入的价格不能为负数,请重新输入"})return false;}if (price.toString().indexOf('.') !== -1 && price.toString().split('.')[1].length > 2) {this.$refs.uToast.show({type: 'error',message: "输入的价格小数点后最多只有两位数字,请重新输入"})return false;}return true;},originalPriceChange() {let haha = this.priceVerify(this.product.originalPrice);if (haha === false) {console.log("haha:" + haha);this.product.originalPrice = 0.00;console.log("this.product" + JSON.stringify(this.product));}},priceChange() {if (this.priceVerify(this.product.price) === false) {this.product.price = 0.00;}},/*** 修改成色* @param {Object} index*/changeFineness(index) {this.product.fineness = index;},/*** 修改功能状态* @param {Object} index*/changeFunctionalStatus(index) {this.product.functionalStatus = index;},/*** 上传闲置商品*/uploadSellProduct() {// console.log("上传闲置商品picList:" + JSON.stringify(this.picList));if (this.product.productCategoryId) {if (this.picList.length == 0) {this.$refs.uToast.show({type: 'error',message: "商品图片没有上传成功"})} else {this.setPicAspectRatio().then(() => {// console.log("即将上传的商品:" + JSON.stringify(this.product));uploadSellProduct(this.product).then(res => {this.$refs.uToast.show({type: 'success',message: "您的商品已经发布到平台"})setTimeout(() => {uni.reLaunch({url: "/pages/index/index"})}, 1000)}).catch(error => {console.log("error:" + JSON.stringify(error));this.$refs.uToast.show({type: 'error',message: "商品发布失败"})});});}} else {this.$refs.uToast.show({type: 'error',message: "请选择分类"})}},/*** 设置图片的宽高比*/setPicAspectRatio() {return new Promise((resolve, reject) => {this.product.picList = [];let promises = [];for (let i = 0; i < this.picList.length; i++) {let picUrl = this.picList[i];promises.push(this.getAspectRatio(picUrl).then((res) => {let pic = {address: picUrl,aspectRatio: res}this.product.picList.push(pic);console.log("当前图片高宽比设置完成");}))}Promise.all(promises).then(() => {console.log("所有图片高宽比设置完成,this.product.picList:" + JSON.stringify(this.product.picList));resolve();})})},/*** 获取单个图片的高宽比* @param {Object} url*/getAspectRatio(url) {return new Promise((resolve, reject) => {uni.getImageInfo({src: url,success: function(res) {let aspectRatio = res.height / res.width;resolve(aspectRatio);}});})},/*** 选择分类*/selectCategory() {uni.navigateTo({url: "/pages/sellMyProduct/selectCategory"})}}}
</script><style lang="scss">.container {background: #F6F6F6;min-height: 100vh;padding: 20rpx;.content {background: #ffffff;padding: 20rpx;.item {display: flex;align-items: center;height: 50px;margin-bottom: 5px;.labelName {width: 70px;margin-right: 10px;}.textClass {display: inline;background: #F7F7F7;padding: 10px;margin-right: 15px;border-radius: 5px;}.selectTextClass {display: inline;background: #2B92FF;padding: 10px;margin-right: 15px;border-radius: 5px;color: #ffffff;font-weight: bold;}.columnClass {// height: 50px;display: flex;align-items: center;width: calc(100% - 70px);overflow-x: auto;// // 让内容只有一行white-space: nowrap;}.columnClass::-webkit-scrollbar {background-color: transparent;/* 设置滚动条背景颜色 */// width: 0px;height: 0px;}}}}
</style>
相关文章:
【UniApp开发小程序】小程序首页完善(滑到底部数据翻页、回到顶端、基于回溯算法的两列数据高宽比平衡)【后端基于若依管理系统开发】
文章目录 说明细节一:首页滑动到底部,需要查询下一页的商品界面预览页面实现 细节二:当页面滑动到下方,出现一个回到顶端的悬浮按钮细节三:商品分列说明优化前后效果对比使用回溯算法实现ControllerService回溯算法 优…...
使用errors.Wrapf()代替log.Error()
介绍不同语言的错误处理机制: Error handling patterns[1] Musings about error handling mechanisms in programming languages[2] 项目中 main调func1,func1调取func2... 这样就会出现很多的 if err ! nil { log.Printf()} , 在Kibana上查看时会搜到多条日志, 需要…...
企业面临的IP风险,如何应对?
IP风险画像为企业或组织在知识产权领域面临的潜在风险和威胁的综合概览。通过对相关知识产权的保护和管理,企业可以预测和应对潜在的法律、商业和声誉风险。 IP数据云帮助企业更好地了解和应对知识产权方面的风险。并提供了关于当前全球知识产权环境的重要信息&…...
L1-046 整除光棍(Python实现) 测试点全过
题目 这里所谓的“光棍”,并不是指单身汪啦~ 说的是全部由1组成的数字,比如1、11、111、1111等。传说任何一个光棍都能被一个不以5结尾的奇数整除。比如,111111就可以被13整除。 现在,你的程序要读入一个整数x,这个整…...
《Web安全基础》04. 文件上传漏洞
web 1:文件上传漏洞2:WAF 绕过2.1:数据溢出2.2:符号变异2.3:数据截断2.4:重复数据 本系列侧重方法论,各工具只是实现目标的载体。 命令与工具只做简单介绍,其使用另见《安全工具录》…...
文本匹配实战系列
引言 本系列文章开始介绍深度学习在文本匹配领域的应用,并且会尝试得到各种模型在给定的数据集上的表现。 深度文本匹配发展比较久,积累了很多文本匹配方法。也有很多的分类方式,一种分类方式是表示型和交互型。 表示型方法 表示型(repre…...
【Kafka】Kafka Stream简单使用
一、实时流式计算 1. 概念 一般流式计算会与批量计算相比较。在流式计算模型中,输入是持续的,可以认为在时间上是无界的,也就意味着,永远拿不到全量数据去做计算。同时,计算结果是持续输出的,也即计算结果…...
在Linux服务器上,查看系统最近的重启记录
在Linux服务器上,您可以查看系统的重启记录以了解系统何时进行了重启。系统的重启记录通常被记录在系统日志文件中。以下是在不同Linux发行版上查看系统重启记录的方法: 1. 使用 last 命令: 打开终端,并输入以下命令来查看系统的…...
Vue2023 面试归纳及复习
1. Vue 3中的Composition API(Hooks)是什么?它与Options API有何不同? 答:Composition API是Vue 3中引入的一种新的API风格, 用于组织和重用组件逻辑。它与Options API相比, 提供了更灵活和可…...
Android动态可编辑长度列表
概述 在界面实现一个列表,用户可以随意给列表新增或者删除项目,在开发中比较常用,但是真正做起来又有点花时间,今天花时间做一个,以便在以后的开发中用到。 详细 运行效果: 二、实现思路: 1…...
合并对象在 Typescript 中的实现与应用
合并对象在 Typescript 中的实现与应用 文章目录 合并对象在 Typescript 中的实现与应用一、简介二、实现1、函数实现2、参数说明3、返回值 三、使用示例四、实际应用场景五、拓展:使用 lodash-es 的 assign 函数进行对象合并1、简介2、安装与导入3、基础用法4、注意…...
antd upload组件beforeUpload返回promise之后,获取的文件不是file类型导致上传失败
之前的beforeUpload直接返回一个false值 ,文件是可以正常与服务端进行传输的 beforeUpload: (file) > {return false},但是这样并不能阻止文件上传,看了官方文档后,改用返回promise对象上传 beforeUpload: (file) > {console.log(-befo…...
创建ffmpeg vs2019工程
0 写在前面 本文主要参考链接:https://www.cnblogs.com/suiyek/p/15669562.html 感谢作者的付出; 1 目录结构 2 下载yasm和nasm 如果自己在安装VS2019等IDE的时候已经安装了它们,则不用再单独进行安装,比如我这边已经安装了&a…...
无涯教程-机器学习 - Jupyter Notebook函数
Jupyter笔记本基本上为开发基于Python的数据科学应用程序提供了一个交互式计算环境。它们以前称为ipython笔记本。以下是Jupyter笔记本的一些功能,使其成为Python ML生态系统的最佳组件之一- Jupyter笔记本可以逐步排列代码,图像,文本,输出等内容,从而逐步说明分析过程。 它有…...
ubuntu安装单机的Consul
文章目录 场景解决启动方式 场景 公司使用Consul做注册发现中心以及管理配置,之前没有用过consul, 现在记录下ubuntu部署的过程 解决 apt 安装 wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-…...
聊聊mybatis-plus的sql加载顺序
序 本文主要研究一下如果mybatis mapper定义了多个同名方法会不会有问题 MybatisConfiguration com/baomidou/mybatisplus/core/MybatisConfiguration.java /*** MybatisPlus 加载 SQL 顺序:* <p> 1、加载 XML中的 SQL </p>* <p> 2、加载 SqlP…...
基于jeecg-boot的flowable流程审批时增加下一个审批人设置
更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码: https://gitee.com/nbacheng/nbcio-boot 前端代码:https://gitee.com/nbacheng/nbcio-vue.git 在线演示(包括H5) : http://122.227.135.243:9888 因为有时…...
HTML 与 CSS 有什么区别?
HTML(超文本标记语言)和 CSS(层叠样式表)是构建网页的两个核心技术。HTML负责定义网页的结构和内容,而CSS则用于控制网页的样式和布局。虽然它们在构建网页时密切相关,但它们在功能和用途上有明显的区别。 …...
服务器数据恢复-vmware ESXI虚拟机数据恢复案例
服务器数据恢复环境: 从物理机迁移一台虚拟机到ESXI,迁移后做了一个快照。该虚拟机上部署了一个SQLServer数据库,存放了5年左右的数据。ESXI上有数十台虚拟机,EXSI连接了一台EVA存储,所有的虚拟机都在EVA存储上。 服务…...
Rabbitmq的Shovel
Federation 具备的数据转发功能类似, Shovel 够可靠、持续地从一个 Broker 中的队列 ( 作为源端,即source)拉取数据并转发至另一个 Broker 中的交换器 ( 作为目的端,即 destination) 。作为源端的队列和作为目的端的交换器可以同时位于…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
