目录

前言

真实案例

我的思考

代码实现

核心功能

代码实现

数据库

springboot

后台管理系统

uniapp(小程序)


前言

有人说轮播图功能太简单了,没有必要写一篇文章。我想说的是从来都没有任何一个功能是简单的,这取决于你对功能的理解,通常我在拿个一个功能后,我都会搞清楚,为什么要做这个功能,解决了什么问题?有没有更好的交互方式?有没有更好的技术解决方案?

在与客户对接需求的时候,客户有没有对轮播图提出特殊的要求?如果客户没有提过,作为专业的工程师,有没有主动给客户介绍轮播图的真实用法,真实效果?有没有告诉客户通常轮播图是怎样的效果,怎么做更适合咱们的项目。

写这篇文档,一方面让大家少走一些弯路;一方面对知识的一个总结

真实案例

2020年5月14日,客户说需要做一个轮播图,其他都没有说。但是从接到需求->代码写完->最终验收费了好大的劲,以下是整个过程:

  1. 第一次交付:客户没有说,我就认为是简简单单的图片展示,客户说可以,没问题。
  2. 第二次交付:过了一天,客户说我想把另一张图片放到第一个,你这怎么放不了。
  3. 第三次交付:做好后,客户说你这能不能展示和隐藏,如果突发情况,也好操作。
  4. 第四次交付:你这不能点击啊,别人都能点击看到点击后的内容。于是我做了文本展示
  5. 第五次交付:你这文本不能上传图片,于是我做了兼容富文本
  6. 第六次交付:我看别人的都是可以跳转到公众号链接的,咱们能不能这么做。
  7. 最终这才满意

最后一算,就这个简简单单的轮播图,花了我3天的时间,心理没有点打击是不可能的。

我的思考

这件事完成后,我进行了复盘,一个简简单单的轮播图问题为什么会花费这么长时间。是客户的错吗?是开发人员的错吗?我想客户没有错,错的是身为专业人员没有做到专业的事。

  1. 为什么客户没错?客户一开始没有说清楚要什么样的,并且反复几次后也没有说清楚要什么样的,这样确实挺烦人的,事实上确实也是这样,但是我们扪心自问,如果客户都知道了,还要我们这样的开发人员做什么?如果客户都懂,那么我们不就没有任何价值了吗?
  2. 为什么我们错了?我觉得核心的一点就是【我们是专业的软件工程师】,而客户都是无知的,他们的特点往往是不懂软件,不懂开发,不懂需求。我觉得作为专业人员,应该在拿到一个需求的时候做到心理有数,就轮播图而言,在客户第一次提出需求时,我们就应该心理清楚通常情况下就应该包含哪些功能,即使客户没有提出来,我们也应该耐心去给客户分析和讲解。

代码实现

核心功能

后台管理系统:排序,展示/隐藏,富文本(文字+图片+视频),链接

小程序:展示,跳转链接,跳转富文本

代码实现

数据库

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>

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐