springboot+admin+uniapp(小程序)实现轮播图功能
有人说轮播图功能太简单了,没有必要写一篇文章。我想说的是从来都没有任何一个功能是简单的,这取决于你对功能的理解,通常我在拿个一个功能后,我都会搞清楚,为什么要做这个功能,解决了什么问题?有没有更好的交互方式?有没有更好的技术解决方案?
·
目录
前言
有人说轮播图功能太简单了,没有必要写一篇文章。我想说的是从来都没有任何一个功能是简单的,这取决于你对功能的理解,通常我在拿个一个功能后,我都会搞清楚,为什么要做这个功能,解决了什么问题?有没有更好的交互方式?有没有更好的技术解决方案?
在与客户对接需求的时候,客户有没有对轮播图提出特殊的要求?如果客户没有提过,作为专业的工程师,有没有主动给客户介绍轮播图的真实用法,真实效果?有没有告诉客户通常轮播图是怎样的效果,怎么做更适合咱们的项目。
写这篇文档,一方面让大家少走一些弯路;一方面对知识的一个总结
真实案例
2020年5月14日,客户说需要做一个轮播图,其他都没有说。但是从接到需求->代码写完->最终验收费了好大的劲,以下是整个过程:
- 第一次交付:客户没有说,我就认为是简简单单的图片展示,客户说可以,没问题。
- 第二次交付:过了一天,客户说我想把另一张图片放到第一个,你这怎么放不了。
- 第三次交付:做好后,客户说你这能不能展示和隐藏,如果突发情况,也好操作。
- 第四次交付:你这不能点击啊,别人都能点击看到点击后的内容。于是我做了文本展示
- 第五次交付:你这文本不能上传图片,于是我做了兼容富文本
- 第六次交付:我看别人的都是可以跳转到公众号链接的,咱们能不能这么做。
- 最终这才满意
最后一算,就这个简简单单的轮播图,花了我3天的时间,心理没有点打击是不可能的。
我的思考
这件事完成后,我进行了复盘,一个简简单单的轮播图问题为什么会花费这么长时间。是客户的错吗?是开发人员的错吗?我想客户没有错,错的是身为专业人员没有做到专业的事。
- 为什么客户没错?客户一开始没有说清楚要什么样的,并且反复几次后也没有说清楚要什么样的,这样确实挺烦人的,事实上确实也是这样,但是我们扪心自问,如果客户都知道了,还要我们这样的开发人员做什么?如果客户都懂,那么我们不就没有任何价值了吗?
- 为什么我们错了?我觉得核心的一点就是【我们是专业的软件工程师】,而客户都是无知的,他们的特点往往是不懂软件,不懂开发,不懂需求。我觉得作为专业人员,应该在拿到一个需求的时候做到心理有数,就轮播图而言,在客户第一次提出需求时,我们就应该心理清楚通常情况下就应该包含哪些功能,即使客户没有提出来,我们也应该耐心去给客户分析和讲解。
代码实现
核心功能
后台管理系统:排序,展示/隐藏,富文本(文字+图片+视频),链接
小程序:展示,跳转链接,跳转富文本
代码实现
数据库
DROP TABLE IF EXISTS `com_lunbotu`;
CREATE TABLE `com_lunbotu` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '主键id',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '修改人',
`del_flag` int(11) NOT NULL DEFAULT 0 COMMENT '删除标志 默认0',
`img_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '图片地址',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '名字。富文本或链接的页面标题',
`content_type` int(255) NULL DEFAULT 0 COMMENT '内容类型:1.无(无法点击);2.富文本;3.链接',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '根据内容类型定',
`state` int(11) NULL DEFAULT 1 COMMENT '状态:1.发布;0.未发布',
`sort` tinyint(4) NULL DEFAULT 0 COMMENT '排序',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户' ROW_FORMAT = DYNAMIC;
springboot
我在项目中使用的是jap+mybatis,这里只展示核心的entity的代码
package com.yanwei.tmpl.module.common.lunbotu;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yanwei.tmpl.module.common.jpa.HighsetBaseEntity;
import com.yanwei.tmpl.module.common.lunbotu.model.ComLunbotuAddOrEditParamModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Data
@Entity
@Table(name = "com_lunbotu")
@TableName("com_lunbotu")
@SQLDelete(sql = "update com_lunbotu set del_flag= 1 where id =?")
@SQLDeleteAll(sql = "update com_lunbotu set del_flag= 1 where id =?")
@Where(clause = "del_flag=0")
@DynamicInsert
@DynamicUpdate
public class ComLunbotuEntity extends HighsetBaseEntity {
@ApiModelProperty(value = "图片地址")
@Column(name = "img_url")
private String imgUrl;
@ApiModelProperty(value = "内容类型:1.无(无法点击);2.富文本;3.链接")
@Column(name = "content_type")
private Integer contentType;
@ApiModelProperty(value = "根据内容类型定")
@Column(name = "content")
private String content;
@ApiModelProperty(value = "状态:1.发布;0.未发布")
@Column(name = "state")
private Integer state;
@ApiModelProperty(value = "名字。富文本或链接的页面标题")
@Column(name = "name")
private String name;
@ApiModelProperty(value = "sort")
@Column(name = "sort")
private Integer sort;
public void setData(ComLunbotuAddOrEditParamModel paramModel){
this.imgUrl = paramModel.getImgUrl();
this.contentType = paramModel.getContentType();
this.content = paramModel.getContent();
this.state = paramModel.getState();
this.name = paramModel.getName();
this.sort = paramModel.getSort();
}
}
package com.yanwei.tmpl.module.common.jpa;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"})
public class HighsetBaseEntity implements Serializable {
@Id
@GenericGenerator(name = "system_uuid",strategy = "uuid")
@GeneratedValue(generator = "system_uuid" , strategy = GenerationType.AUTO)
@Column(name = "id" , unique = true , nullable = false,length = 32)
private String id;
@CreatedBy
@Column(name = "create_by")
private String createBy;
@CreatedDate
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "create_time")
private Date createTime;
@LastModifiedBy
@Column(name = "update_by")
private String updateBy;
@LastModifiedDate
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "update_time")
private Date updateTime;
@Column(name = "del_flag")
private Integer delFlag = CommonConstant.STATUS_NORMAL;
}
后台管理系统
<template>
<el-card>
<el-form :model="searchForm" inline ref="searchForm">
<el-form-item label="名称" prop="name">
<el-input v-model="searchForm.name" clearable placeholder=""></el-input>
</el-form-item>
<el-form-item label="状态" prop="state">
<el-select v-model="searchForm.state" clearable placeholder="" style="wdith:100%">
<el-option value="1" label="发布"></el-option>
<el-option value="0" label="未发布"></el-option>
</el-select>
</el-form-item>
</el-form>
<div>
<el-button icon="el-icon-search" type="primary" size="mini" @click="searchClick"
>搜索</el-button
>
<el-button icon="el-icon-refresh" type="warning" size="mini" @click="resetClick"
>重置</el-button
>
<el-button
size="mini"
icon="el-icon-plus"
type="primary"
plain
@click="addDialog = true"
>新增</el-button
>
</div>
<!-- 表格渲染 -->
<custom-table
:tableInfo="tableInfo"
:tableProps="tableProps"
:pageSize="searchForm.pageSize"
@handleSizeChange="handleSizeChange"
@handleCurrentChange="handleCurrentChange"
>
<el-table-column slot="table_oper" label="操作">
<template v-slot="scope">
<el-button
type="text"
icon="el-icon-edit"
@click="editShowClick(scope.row)"
>修改</el-button
>
<el-button
type="text"
icon="el-icon-delete"
@click="deleteClick(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</custom-table>
<!-- 添加 -->
<el-dialog
title="添加"
v-if="addDialog"
:visible.sync="addDialog"
class="over-loading"
width="70%"
destroy-on-close
:close-on-press-escape="false"
:close-on-click-modal="false"
>
<div class="dialog-content">
<el-form :model="addForm" ref="addForm" label-width="95px" :show-message="false">
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="name" required>
<el-input v-model="addForm.name" placeholder="" style="width: 300px"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="state" required>
<el-select v-model="addForm.state" placeholder="" style="width: 300px">
<el-option value="1" label="发布"></el-option>
<el-option value="0" label="未发布"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="图片" prop="imgUrl" required>
<el-upload
class="avatar-uploader"
:headers="uploaderConfig.headers"
:action="uploaderConfig.action"
:data="uploaderConfig.data"
:name="uploaderConfig.name"
:accept="uploaderConfig.accept"
:show-file-list="false"
:on-success="addUploaderSuccess">
<img v-if="addForm.imgUrl" :src="addForm.imgUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="sort" required>
<el-input-number
v-model="addForm.sort"
:max="100"
:min="0"
controls-position="right"
style="width: 300px;"
></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-form-item label="内容类型" prop="contentType" required>
<el-select v-model="addForm.contentType" placeholder="" style="width: 300px">
<el-option value="1" label="无内容"></el-option>
<el-option value="2" label="富文本"></el-option>
</el-select>
</el-form-item>
</el-row>
<el-row v-if="addForm.contentType == 2">
<el-col>
<el-form-item label="内容" prop="content" >
<ywEditor
ref="yeEditorAddRef"
fileRootPath="base/lunbotu"
:content="addForm.content"
@onChange="ywEditorChange($event,'add')"></ywEditor>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="addDialog = false">取消</el-button>
<el-button type="primary" @click="addSubmitClick"> 确 定 </el-button>
</div>
</el-dialog>
<!-- 修改 -->
<el-dialog
title="修改"
v-if="editDialog"
:visible.sync="editDialog"
class="over-loading"
width="70%"
destroy-on-close
:close-on-press-escape="false"
:close-on-click-modal="false"
>
<div class="dialog-content">
<el-form :model="editForm" ref="editForm" label-width="95px" :show-message="false">
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="name" required>
<el-input v-model="editForm.name" placeholder="" style="width: 300px"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="state" required>
<el-select v-model="editForm.state" placeholder="" style="width: 300px">
<el-option value="1" label="发布"></el-option>
<el-option value="0" label="未发布"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="图片" prop="imgUrl" required>
<el-upload
class="avatar-uploader"
:headers="uploaderConfig.headers"
:action="uploaderConfig.action"
:data="uploaderConfig.data"
:name="uploaderConfig.name"
:accept="uploaderConfig.accept"
:show-file-list="false"
:on-success="editUploaderSuccess">
<img v-if="editForm.imgUrl" :src="editForm.imgUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="sort" required>
<el-input-number
v-model="editForm.sort"
:max="100"
:min="0"
controls-position="right"
style="width: 300px;"
></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-form-item label="内容类型" prop="contentType" required>
<el-select v-model="editForm.contentType" placeholder="" style="width: 300px">
<el-option value="1" label="无内容"></el-option>
<el-option value="2" label="富文本"></el-option>
</el-select>
</el-form-item>
</el-row>
<el-row v-if="editForm.contentType == 2">
<el-col>
<el-form-item label="内容" prop="content">
<ywEditor ref="yeEditorEditRef" fileRootPath="base/lunbotu"
:content="editForm.content"></ywEditor>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="editDialog = false">取消</el-button>
<el-button type="primary" @click="editSubmitClick"> 确 定 </el-button>
</div>
</el-dialog>
</el-card>
</template>
<script>
import { Message } from "element-ui";
import { baseLunbotuFindAllByPage, baseLunbotuEdit, baseLunbotuAdd, baseLunbotuDelById } from "../../http/api";
import CustomTable from "../../components/CustomTable/CustomTable.vue";
import ywEditor from "../../components/ywEditor/ywEditor.vue"
import http from "../../http/http.js"
export default {
components: { CustomTable,ywEditor },
created() {
this.getDataMethod();
},
data() {
return {
//表格数据
tableInfo: {
rows: [],
total: 0,
pageNum: 1,
},
tableProps: [
{ label: "标题", prop: "name" },
{ label: "图片", prop: "imgUrl", type: "picture" },
{ label: "内容类型", prop: "contentType" ,
type: "tag",
result: [
{ code: 1, type: "primary", text: "无" },
{ code: 2, type: "success", text: "富文本" },
],
},
{ label: "排序", prop: "sort" },
{
label: "状态",
prop: "state",
type: "tag",
result: [
{ code: 1, type: "primary", text: "展示" },
{ code: 0, type: "info", text: "未发布" },
],
},
{ label: "操作时间", prop: "createTime" },
],
dataList: [],
searchForm: {
pageSize: 10,
pageNum: 1,
state: null,
name: null,
sort:null,
},
total: 0,
currentPage: 1,
addDialog: false,
addForm: {
name: null,
imgUrl: null,
state:"0",
content:"",
contentType:"1"
},
editDialog: false,
editForm: {},
uploaderConfig:{
headers:{
token: http.getToken()
},
action:http.upFileUrl,
data:{
fileRootPath:"base/lunbotu"
},
name:"file",
accept:".jpg,.jpeg,.png,.JPG,.JPEG,.PNG",
}
};
},
watch: {
addDialog() {
if (!this.addDialog) {
this.$refs.addForm.resetFields();
}
},
},
methods: {
editSubmitClick() {
console.info(this.editForm)
if(this.editForm.contentType == "2"){
this.editForm.content = this.$refs.yeEditorEditRef.getHtml()
}
this.$refs.editForm.validate(async (valid) => {
if (valid) {
const res = await baseLunbotuEdit(this.editForm);
if (res.success) {
Message.success("操作成功");
this.getDataMethod();
this.editDialog = false;
}
} else {
Message.warning("输入必填项");
}
});
},
//展示
editShowClick(val) {
this.editForm = JSON.parse(JSON.stringify(val));
this.editForm.state = "" + this.editForm.state
this.editForm.contentType = "" + this.editForm.contentType
this.editDialog = true;
},
addUploaderSuccess(res, file){
console.info(res,file)
this.addForm.imgUrl = res.result.newFullPath;
},
editUploaderSuccess(res, file){
console.info(res,file)
this.editForm.imgUrl = res.result.newFullPath;
},
//添加
addSubmitClick() {
console.info("添加",this.addForm)
if(this.addForm.contentType == "2"){
this.addForm.content = this.$refs.yeEditorAddRef.getHtml()
}
this.$refs.addForm.validate(async (valid) => {
if (valid) {
const res = await baseLunbotuAdd(this.addForm);
if (res.success) {
Message.success("操作成功");
this.getDataMethod();
this.addDialog = false;
}
} else {
Message.warning("输入必填项");
}
});
},
//每页条数
handleSizeChange(val) {
this.searchForm.pageSize = val;
//调用接口
this.getDataMethod();
},
//页码
handleCurrentChange(val) {
this.searchForm.pageNum = val;
//调用接口
this.getDataMethod();
},
//删除
deleteClick(value) {
this.$confirm("确定删除?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
const res = await baseLunbotuDelById(value.id);
if (res.success) {
Message.success("操作成功");
this.getDataMethod();
}
})
.catch(() => {});
},
//获取数据列表
async getDataMethod() {
const res = await baseLunbotuFindAllByPage(this.searchForm);
if (res.success) {
this.tableInfo = res.result;
}
},
//搜索按钮
searchClick() {
this.searchForm.pageNum = 1;
this.getDataMethod();
},
//重置按钮
resetClick() {
this.$refs.searchForm.resetFields();
this.searchForm.pageNum = 1;
this.getDataMethod();
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .avatar-uploader .el-upload {
height: 130px;
width: 200px;
border: 1px dashed #d9d9d9;
border-radius: unset;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
height: 130px;
width: 200px;
line-height: 130px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
uniapp(小程序)
colorUI + mp-html
<swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000" :circular="true">
<swiper-item v-for="(item,index) in lunbotuList" :key="index">
<image style="width: 100%; height: 100%;border-radius: 10rpx;" :src="item.imgUrl" mode="" @click="lunbotuClick(item,index)"></image>
</swiper-item>
</swiper>
lunbotuClick(item,index){
console.info(item,index)
if(item.contentType == 2){
this.$common.navigateTo("/pages/commons/web-show/web-show",{
type:"HTML",
title:item.name,
content:encodeURIComponent(item.content)
})
}else if(item.contentType == 3){
this.$common.navigateTo("/pages/commons/web-show/web-show",{
type:"URL",
title:item.name,
content:encodeURIComponent(item.content)
})
}
}
web-show.vue
<template>
<view class="bg-white padding-sm">
<web-view v-if="type=='URL'" :src="content"></web-view>
<mp-html v-if="type=='HTML'" :show-img-menu="false" :content="content" />
</view>
</template>
<script>
export default {
data() {
return {
type:"",
content:"",
}
},
onLoad(param) {
console.info(param)
this.type = param.type
if(param.type == 'URL'){
this.content = decodeURIComponent(param.content)
if(this.$common.isNotBlank(param.title)){
uni.setNavigationBarTitle({
title:param.title
})
}
}else if(param.type == 'HTML'){
this.content = decodeURIComponent(param.content)
if(this.$common.isNotBlank(param.title)){
uni.setNavigationBarTitle({
title:param.title
})
}
}
},
methods: {
}
}
</script>
<style>
</style>
更多推荐
已为社区贡献3条内容
所有评论(0)